Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 2 additions & 23 deletions src/megatron/bridge/models/conversion/model_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ def _megatron_local_name_to_global(
# EP — fetched lazily because dense models may not have an EP group at all
# (and for the decentralized PG path, ``pg_collection.ep`` may be ``None``).
# For now adapters are not sharded across EP ranks.
is_grouped_expert_param = ".mlp.experts.linear_fc" in param_name
is_local_expert_param = ".mlp.experts.local_experts." in param_name
is_grouped_expert_param = ".experts.linear_fc" in param_name
is_local_expert_param = ".experts.local_experts." in param_name
is_expert_param = (is_grouped_expert_param or is_local_expert_param) and ".adapter." not in param_name
ep_group = _get_ep_group(models) if is_expert_param else None
if is_expert_param and ep_group is not None and get_pg_size(ep_group) > 1:
Expand Down Expand Up @@ -316,27 +316,6 @@ def _update_grouped_expert_number(param_name: str, param_type: str) -> str:
elif re.search(r"\.bias\d+(?=$|\.)", param_name):
param_name = _update_grouped_expert_number(param_name, "bias")

# EP for SequentialMLP: expert index is in the module path as local_experts.N.
# This covers both standard SequentialMLP (e.g., quantization) and dual-pool MoE
# (e.g., text_moe_layer.experts.local_experts.N or vision_moe_layer.experts.local_experts.N).
elif (
".experts.local_experts." in param_name
and ep_group is not None
and get_pg_size(ep_group) > 1
and ".adapter." not in param_name
):
num_experts = config.num_moe_experts
num_experts_per_rank = num_experts // ep_group.size()

match = re.search(r"\.local_experts\.(\d+)\.", param_name)
if match:
local_expert_number = int(match.group(1))
global_expert_number = num_experts_per_rank * ep_group.rank() + local_expert_number
param_name = param_name.replace(
f".local_experts.{local_expert_number}.",
f".local_experts.{global_expert_number}.",
)

return param_name


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ class ErnieMultiTypeMoE(MegatronModule):
submodules: MultiTypeMoeSubmodules containing specs for both pools.
layer_number: Layer index in the transformer stack.
pg_collection: Process group collection for parallelism.
is_mtp_layer: Whether this MoE is used inside an MTP layer.
name: Optional module instance name passed top-down by Megatron-Core.
"""

def __init__(
Expand All @@ -123,13 +125,16 @@ def __init__(
submodules: Optional[MultiTypeMoeSubmodules] = None,
layer_number: Optional[int] = None,
pg_collection: Optional[ProcessGroupCollection] = None,
is_mtp_layer: bool = False,
name: str | None = None,
):
super().__init__(config=config)
self.layer_number = layer_number
self.is_mtp_layer = is_mtp_layer

# TransformerLayer only passes pg_collection to known MLP types (MoELayer,
# TEGroupedMLP, SequentialMLP). ErnieMultiTypeMoE is not in that list, so
# pg_collection may be None. Fall back to default MoE process groups.
# Older TransformerLayer paths only passed pg_collection to known MLP
# types. If ErnieMultiTypeMoE is instantiated outside the current path,
# pg_collection may still be None. Fall back to default MoE groups.
if pg_collection is None:
pg_collection = get_default_pg_collection()

Expand All @@ -145,13 +150,26 @@ def __init__(
self.vision_config.moe_shared_expert_intermediate_size = None

# Build the two MoE pools and shared experts
self.text_moe_layer = build_module(submodules.text_moe_layer, self.text_config)
self.vision_moe_layer = build_module(submodules.vision_moe_layer, self.vision_config)
self.text_moe_layer = build_module(
submodules.text_moe_layer,
self.text_config,
pg_collection=pg_collection,
is_mtp_layer=is_mtp_layer,
name=(name + ".text_moe_layer") if name is not None else None,
)
self.vision_moe_layer = build_module(
submodules.vision_moe_layer,
self.vision_config,
pg_collection=pg_collection,
is_mtp_layer=is_mtp_layer,
name=(name + ".vision_moe_layer") if name is not None else None,
)
self.shared_experts = build_module(
submodules.shared_experts,
config=config,
pg_collection=pg_collection,
gate=False,
name=(name + ".shared_experts") if name is not None else None,
)

def forward(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# CI_TIMEOUT=60
#!/bin/bash
Comment on lines +1 to +2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: #!/bin/bash on line 2 is a no-op — shebangs only work on line 1. Other CI_TIMEOUT scripts (e.g., L0_Launch_megatron_fsdp.sh) don't include a shebang at all since CI invokes them via bash explicitly. Remove it to stay consistent:

Suggested change
# CI_TIMEOUT=60
#!/bin/bash
# CI_TIMEOUT=60

# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -xeuo pipefail

export CUDA_VISIBLE_DEVICES="0,1"

uv run coverage run --data-file=/opt/Megatron-Bridge/.coverage --source=/opt/Megatron-Bridge/ --parallel-mode -m pytest \
-o log_cli=true -o log_cli_level=INFO -v -s -x -m "not pleasefixme" --tb=short -rA \
tests/functional_tests/test_groups/models/ernie_vl/test_ernie45_vl_conversion.py
coverage combine -q
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ def test_ernie45_vl_forward_backward(
"""
import sys

fwd_bwd_script = str(Path(__file__).parent / "ernie45_vl_fwd_bwd.py")
repo_root = Path(__file__).resolve().parents[5]
fwd_bwd_script = str(repo_root / "examples/models/vlm/ernie_vl/ernie45_vl_fwd_bwd.py")

cmd = [
sys.executable,
Expand Down Expand Up @@ -414,7 +415,7 @@ def test_ernie45_vl_forward_backward(
cmd,
capture_output=True,
text=True,
cwd=Path(__file__).parent.parent.parent.parent.parent.parent,
cwd=repo_root,
timeout=300,
)

Expand Down
54 changes: 53 additions & 1 deletion tests/unit_tests/models/ernie_vl/test_ernie45_vl_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

"""Unit tests for ERNIE 4.5 VL (Vision-Language) MoE bridge."""

from unittest.mock import Mock
from types import SimpleNamespace
from unittest.mock import Mock, patch

import pytest
import torch
Expand All @@ -28,6 +29,10 @@
_OffsetRowParallelMapping,
)
from megatron.bridge.models.ernie_vl.ernie45_vl_provider import Ernie45VLModelProvider
from megatron.bridge.models.ernie_vl.modeling_ernie45_vl.ernie_moe_layer import (
ErnieMultiTypeMoE,
MultiTypeMoeSubmodules,
)


def _make_vision_config():
Expand Down Expand Up @@ -477,3 +482,50 @@ def test_has_expected_fields(self):
assert hasattr(provider, "num_layers")
assert hasattr(provider, "hidden_size")
assert hasattr(provider, "num_attention_heads")


class TestErnieMultiTypeMoE:
"""Test ERNIE VL dual-pool MoE construction."""

def test_accepts_transformer_layer_kwargs(self):
config = SimpleNamespace(moe_intermediate_size=(64, 32), moe_shared_expert_intermediate_size=128)
submodules = MultiTypeMoeSubmodules(
text_moe_layer=object(),
vision_moe_layer=object(),
shared_experts=object(),
)
pg_collection = object()

with patch("megatron.bridge.models.ernie_vl.modeling_ernie45_vl.ernie_moe_layer.build_module") as build_module:
build_module.side_effect = [Mock(), Mock(), Mock()]
layer = ErnieMultiTypeMoE(
config=config,
submodules=submodules,
layer_number=2,
pg_collection=pg_collection,
is_mtp_layer=True,
name="decoder.layers.1.mlp",
)

assert layer.layer_number == 2
assert layer.is_mtp_layer is True

text_call, vision_call, shared_call = build_module.call_args_list
assert text_call.args[0] is submodules.text_moe_layer
assert text_call.args[1].moe_ffn_hidden_size == 64
assert text_call.kwargs["pg_collection"] is pg_collection
assert text_call.kwargs["is_mtp_layer"] is True
assert text_call.kwargs["name"] == "decoder.layers.1.mlp.text_moe_layer"

assert vision_call.args[0] is submodules.vision_moe_layer
assert vision_call.args[1].moe_ffn_hidden_size == 32
assert vision_call.kwargs["pg_collection"] is pg_collection
assert vision_call.kwargs["is_mtp_layer"] is True
assert vision_call.kwargs["name"] == "decoder.layers.1.mlp.vision_moe_layer"

assert shared_call.args[0] is submodules.shared_experts
assert shared_call.kwargs["config"] is config
assert shared_call.kwargs["pg_collection"] is pg_collection
assert shared_call.kwargs["gate"] is False
assert shared_call.kwargs["name"] == "decoder.layers.1.mlp.shared_experts"
assert "is_mtp_layer" not in shared_call.kwargs
14 changes: 14 additions & 0 deletions tests/unit_tests/models/test_qat_bridge_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ def test_local_expert_name_maps_to_global_expert_rank(monkeypatch) -> None:
assert global_name == "decoder.layers.0.mlp.experts.local_experts.5.linear_fc1.weight"


def test_nested_local_expert_name_maps_to_global_expert_rank(monkeypatch) -> None:
monkeypatch.setattr(model_bridge, "get_pg_size", lambda group: group.size())
model = _FakeModel(ep_size=2, ep_rank=1)
config = SimpleNamespace(num_moe_experts=4)

global_name = model_bridge._megatron_local_name_to_global(
[model],
config,
"decoder.layers.0.mlp.vision_moe_layer.experts.local_experts.1.linear_fc1.weight",
)

assert global_name == "decoder.layers.0.mlp.vision_moe_layer.experts.local_experts.3.linear_fc1.weight"


def test_local_expert_name_mapping_skips_adapter_params(monkeypatch) -> None:
monkeypatch.setattr(model_bridge, "get_pg_size", lambda group: group.size())
model = _FakeModel(ep_size=4, ep_rank=2)
Expand Down
Loading