Skip to content

Commit 2086ee6

Browse files
hbhflw2000yaoyu-33yuekaizhang
authored
[model, bugfix] qwen3-omni: fix Megatron parity for external RL integration (#4988)
Signed-off-by: hbhflw2000 <417911774@qq.com> Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> Co-authored-by: yaoyu-33 <yaoyu.094@gmail.com> Co-authored-by: Yuekai Zhang <zhangyuekai@foxmail.com>
1 parent 2a030ca commit 2086ee6

14 files changed

Lines changed: 415 additions & 12 deletions

File tree

‎examples/models/qwen/qwen3_omni/local_train_thinker_full.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,12 @@ PY
255255

256256
mkdir -p "${THINKER_ONLY_MIRROR_DIR}"
257257
EFFECTIVE_HF_MODEL_PATH=$(prepare_thinker_only_hf_path "${HF_MODEL_PATH}" "${THINKER_ONLY_MIRROR_DIR}" "${PYTHON_BIN}")
258+
# Recipe construction calls AutoBridge before CLI overrides are applied. Point
259+
# it at the local thinker-only mirror so all ranks avoid Hub access and its
260+
# shared config lock.
261+
export QWEN3_OMNI_HF_PATH="${EFFECTIVE_HF_MODEL_PATH}"
262+
export HF_HUB_OFFLINE=1
263+
export TRANSFORMERS_OFFLINE=1
258264

259265
CMD=(
260266
"${PYTHON_BIN}" -m torch.distributed.run

‎src/megatron/bridge/models/qwen_omni/qwen3_omni_bridge.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ def provider_bridge(self, hf_pretrained: PreTrainedCausalLM) -> Qwen3OmniModelPr
104104
spatial_merge_size=getattr(vision_config, "spatial_merge_size", 2),
105105
position_embedding_type="mrope",
106106
mrope_section=rope_scaling.get("mrope_section", [24, 20, 20]),
107+
# HF/vLLM Qwen3-Omni expose interleaved keys in rope_scaling, but the
108+
# thinker text path matches Megatron's non-interleaved rotary layout.
109+
rotary_interleaved=False,
107110
)
108111
return provider
109112

‎src/megatron/bridge/models/qwen_omni/qwen3_omni_provider.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@
4242
from megatron.bridge.models.qwen_vl.modelling_qwen3_vl.attention import Qwen3VLSelfAttention
4343

4444

45+
def _bool_config_enabled(value) -> bool:
46+
if isinstance(value, str):
47+
return value.lower() in {"1", "true", "yes", "on"}
48+
return bool(value)
49+
50+
4551
def _use_qwen3_vl_self_attention(layer_spec) -> None:
4652
"""Install the mRoPE-aware Qwen3-VL attention module on standard attention specs."""
4753
if layer_spec is None:
@@ -98,9 +104,12 @@ class Qwen3OmniModelProvider(GPTModelProvider):
98104
moe_router_load_balancing_type: str = "aux_loss"
99105
moe_aux_loss_coeff: float = 1e-3
100106
moe_router_pre_softmax: bool = False
107+
moe_router_dtype: str = "fp32"
108+
moe_router_score_function: str = "softmax"
101109
moe_token_dispatcher_type: str = "alltoall"
102110
moe_permute_fusion: bool = True
103111
masked_softmax_fusion: bool = False
112+
moe_enable_routing_replay: bool = False
104113

105114
image_token_id: int = 151655
106115
video_token_id: int = 151656
@@ -146,6 +155,11 @@ def provide(self, pre_process=None, post_process=None, vp_stage=None):
146155
)
147156

148157
use_local_attention = self.attention_backend in {AttnBackend.local, "local"}
158+
if use_local_attention and _bool_config_enabled(self.sequence_parallel):
159+
raise ValueError(
160+
"Qwen3-Omni sequence_parallel requires a Transformer Engine attention backend; "
161+
"attention_backend=local selects the torch local attention path."
162+
)
149163
if HAVE_TE and not use_local_attention:
150164
language_transformer_layer_spec = get_gpt_layer_with_transformer_engine_spec(
151165
num_experts=self.num_moe_experts,

‎src/megatron/bridge/models/qwen_vl/modelling_qwen3_vl/attention.py‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import torch
1717
from einops import rearrange
18+
from megatron.core.extensions.transformer_engine import TEDotProductAttention
1819
from megatron.core.transformer.attention import (
1920
HAVE_FA3,
2021
BaseInferenceContext,
@@ -31,6 +32,23 @@
3132
from megatron.bridge.models.qwen_vl.modelling_qwen3_vl.rope import apply_rotary_pos_emb_absolute
3233

3334

35+
def _qwen_attention_mask_for_core_attention(
36+
core_attention: object,
37+
attention_mask: Tensor | None,
38+
packed_seq_params: PackedSeqParams | None,
39+
) -> Tensor | None:
40+
"""Translate Qwen's 2D valid-token mask to TE's masked-token convention."""
41+
if (
42+
not isinstance(core_attention, TEDotProductAttention)
43+
or packed_seq_params is not None
44+
or not isinstance(attention_mask, Tensor)
45+
or attention_mask.ndim != 2
46+
or attention_mask.dtype != torch.bool
47+
):
48+
return attention_mask
49+
return (~attention_mask).unsqueeze(1).unsqueeze(1)
50+
51+
3452
class Qwen3VLSelfAttention(SelfAttention):
3553
"""
3654
Overrides the SelfAttention class, the difference is that qwen3vl uses apply_rotary_pos_emb_absolute
@@ -239,6 +257,11 @@ def forward(
239257
# ==================================
240258

241259
nvtx_range_push(suffix="core_attention")
260+
core_attention_mask = _qwen_attention_mask_for_core_attention(
261+
self.core_attention,
262+
attention_mask,
263+
packed_seq_params,
264+
)
242265
if (
243266
isinstance(self.core_attention, DotProductAttention)
244267
and packed_seq_params is None
@@ -252,7 +275,7 @@ def forward(
252275
query,
253276
key,
254277
value,
255-
attention_mask,
278+
core_attention_mask,
256279
attn_mask_type=attn_mask_type,
257280
attention_bias=attention_bias,
258281
packed_seq_params=packed_seq_params,
@@ -264,7 +287,7 @@ def forward(
264287
query,
265288
key,
266289
value,
267-
attention_mask,
290+
core_attention_mask,
268291
attn_mask_type=attn_mask_type,
269292
attention_bias=attention_bias,
270293
packed_seq_params=packed_seq_params,

‎src/megatron/bridge/models/qwen_vl/modelling_qwen3_vl/rope.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,7 @@ def get_packed_seq_attention_mask(input_ids: torch.Tensor, packed_seq_params: Pa
9696

9797

9898
class Qwen3VLMultimodalRotaryEmbedding(nn.Module):
99-
"""Multimodal Rotary Embedding for language model.
100-
only support for qwen3vl
99+
"""Multimodal Rotary Embedding for Qwen3-VL/Qwen3-Omni language model.
101100
102101
Args:
103102
kv_channels (int): Projection weights dimension in multi-head attention. Obtained
@@ -127,7 +126,6 @@ def __init__(
127126
if rotary_percent < 1.0:
128127
dim = int(dim * rotary_percent)
129128
self.rotary_interleaved = rotary_interleaved
130-
assert not self.rotary_interleaved, "only support qwen3vl"
131129

132130
self.seq_len_interpolation_factor = seq_len_interpolation_factor
133131
self.inv_freq = 1.0 / (

‎src/megatron/bridge/recipes/qwen_omni/h100/qwen3_omni.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__ import annotations
1818

19+
import os
1920
from typing import TYPE_CHECKING
2021

2122
import torch
@@ -34,6 +35,11 @@
3435
_QWEN3_OMNI_HF_PATH = "Qwen/Qwen3-Omni-30B-A3B-Instruct"
3536

3637

38+
def _qwen3_omni_hf_path() -> str:
39+
"""Resolve an explicit local checkpoint before falling back to the public id."""
40+
return os.environ.get("QWEN3_OMNI_HF_PATH", _QWEN3_OMNI_HF_PATH)
41+
42+
3743
def qwen3_omni_30b_a3b_sft_8gpu_h100_bf16_config() -> "ConfigContainer":
3844
"""Return an 8-GPU thinker-only SFT config for Qwen3-Omni 30B-A3B.
3945
@@ -42,7 +48,8 @@ def qwen3_omni_30b_a3b_sft_8gpu_h100_bf16_config() -> "ConfigContainer":
4248

4349
cfg = _sft_common_vlm()
4450

45-
cfg.model = AutoBridge.from_hf_pretrained(_QWEN3_OMNI_HF_PATH).to_megatron_provider(load_weights=False)
51+
hf_model_path = _qwen3_omni_hf_path()
52+
cfg.model = AutoBridge.from_hf_pretrained(hf_model_path).to_megatron_provider(load_weights=False)
4653
cfg.model.seq_length = 4096
4754
cfg.model.tensor_model_parallel_size = 1
4855
cfg.model.pipeline_model_parallel_size = 1
@@ -78,7 +85,7 @@ def qwen3_omni_30b_a3b_sft_8gpu_h100_bf16_config() -> "ConfigContainer":
7885
cfg.scheduler = scheduler_cfg
7986

8087
cfg.dataset.seq_length = 4096
81-
cfg.dataset.hf_processor_path = _QWEN3_OMNI_HF_PATH
88+
cfg.dataset.hf_processor_path = hf_model_path
8289
cfg.dataset.enable_in_batch_packing = False
8390
cfg.dataset.skip_getting_attention_mask_from_dataset = False
8491

@@ -105,7 +112,7 @@ def qwen3_omni_30b_a3b_sft_8gpu_h100_bf16_hf_json_config() -> "ConfigContainer":
105112
cfg.dataset = DirectHFSFTDatasetConfig(
106113
seq_length=cfg.model.seq_length,
107114
preprocessing=ChatSFTPreprocessingConfig(),
108-
hf_processor_path=_QWEN3_OMNI_HF_PATH,
115+
hf_processor_path=_qwen3_omni_hf_path(),
109116
source=HFDatasetSourceConfig(
110117
path_or_dataset="json",
111118
split="train",

‎tests/unit_tests/models/qwen_omni/test_qwen3_omni_bridge.py‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ def mock_text_config():
4141
text_config.max_position_embeddings = 32768
4242
text_config.rope_theta = 1000000.0
4343
text_config.attention_bias = False
44-
text_config.rope_scaling = {"mrope_section": [24, 20, 20]}
44+
text_config.rope_scaling = {
45+
"rope_type": "default",
46+
"interleaved": True,
47+
"mrope_interleaved": True,
48+
"mrope_section": [24, 20, 20],
49+
}
4550
return text_config
4651

4752

@@ -104,6 +109,7 @@ def test_provider_bridge_basic_config(self, mock_hf_pretrained):
104109
assert provider.moe_router_topk == 8
105110
assert provider.share_embeddings_and_output_weights is False
106111
assert provider.mrope_section == [24, 20, 20]
112+
assert provider.rotary_interleaved is False
107113
assert provider.language_max_sequence_length == 32768
108114
assert provider.patch_size == 32
109115
assert provider.temporal_patch_size == 4
@@ -145,6 +151,56 @@ def test_mapping_registry(self):
145151
)
146152
assert any("thinker.language_model.decoder.layers.*.mlp.router.weight" in name for name in mapping_names)
147153

154+
def test_mapping_registry_resolves_parallel_parameter_names(self):
155+
bridge = Qwen3OmniBridge()
156+
registry = bridge.mapping_registry()
157+
158+
expected = {
159+
"thinker.language_model.decoder.layers.0.self_attention.linear_qkv.weight": {
160+
"q": "thinker.model.layers.0.self_attn.q_proj.weight",
161+
"k": "thinker.model.layers.0.self_attn.k_proj.weight",
162+
"v": "thinker.model.layers.0.self_attn.v_proj.weight",
163+
},
164+
"thinker.language_model.decoder.layers.0.self_attention.linear_qkv.layer_norm_weight": (
165+
"thinker.model.layers.0.input_layernorm.weight"
166+
),
167+
"thinker.language_model.decoder.layers.0.input_layernorm.weight": (
168+
"thinker.model.layers.0.input_layernorm.weight"
169+
),
170+
"thinker.language_model.decoder.layers.0.pre_mlp_layernorm.weight": (
171+
"thinker.model.layers.0.post_attention_layernorm.weight"
172+
),
173+
"thinker.language_model.decoder.layers.0.self_attention.q_layernorm.weight": (
174+
"thinker.model.layers.0.self_attn.q_norm.weight"
175+
),
176+
"thinker.language_model.decoder.layers.0.self_attention.k_layernorm.weight": (
177+
"thinker.model.layers.0.self_attn.k_norm.weight"
178+
),
179+
"thinker.language_model.decoder.layers.0.self_attention.linear_proj.weight": (
180+
"thinker.model.layers.0.self_attn.o_proj.weight"
181+
),
182+
"thinker.language_model.decoder.layers.0.mlp.router.weight": "thinker.model.layers.0.mlp.gate.weight",
183+
"thinker.language_model.decoder.layers.0.mlp.experts.linear_fc1.weight7": {
184+
"gate": "thinker.model.layers.0.mlp.experts.7.gate_proj.weight",
185+
"up": "thinker.model.layers.0.mlp.experts.7.up_proj.weight",
186+
},
187+
"thinker.language_model.decoder.layers.0.mlp.experts.linear_fc2.weight7": (
188+
"thinker.model.layers.0.mlp.experts.7.down_proj.weight"
189+
),
190+
"thinker.language_model.decoder.layers.0.mlp.experts.local_experts.7.linear_fc1.weight": {
191+
"gate": "thinker.model.layers.0.mlp.experts.7.gate_proj.weight",
192+
"up": "thinker.model.layers.0.mlp.experts.7.up_proj.weight",
193+
},
194+
"thinker.language_model.decoder.layers.0.mlp.experts.local_experts.7.linear_fc2.weight": (
195+
"thinker.model.layers.0.mlp.experts.7.down_proj.weight"
196+
),
197+
}
198+
199+
for megatron_param, hf_param in expected.items():
200+
mapping = registry.megatron_to_hf_lookup(megatron_param)
201+
assert mapping is not None, megatron_param
202+
assert mapping.hf_param == hf_param
203+
148204
def test_provider_bridge_warns_for_audio_output_stack(self, mock_hf_pretrained, caplog):
149205
mock_hf_pretrained.config.enable_audio_output = True
150206
caplog.set_level("WARNING")

‎tests/unit_tests/models/qwen_omni/test_qwen3_omni_provider.py‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@
3030

3131

3232
class TestQwen3OmniModelProvider:
33+
@pytest.mark.parametrize(
34+
("value", "expected"),
35+
[("true", True), ("ON", True), ("false", False), (True, True), (0, False)],
36+
)
37+
def test_bool_config_enabled(self, value, expected):
38+
assert qwen3_omni_provider._bool_config_enabled(value) is expected
39+
3340
def test_qwen3_omni_model_provider_initialization(self):
3441
provider = Qwen3OmniModelProvider(
3542
num_layers=48,
@@ -43,7 +50,10 @@ def test_qwen3_omni_model_provider_initialization(self):
4350
assert provider.position_embedding_type == "mrope"
4451
assert provider.scatter_embedding_sequence_parallel is False
4552
assert provider.qk_layernorm is True
53+
assert provider.moe_router_dtype == "fp32"
54+
assert provider.moe_router_score_function == "softmax"
4655
assert provider.masked_softmax_fusion is False
56+
assert provider.moe_enable_routing_replay is False
4757
assert provider.apply_rotary_pos_emb_in_fp32 is False
4858
assert provider.image_token_id == 151655
4959
assert provider.video_token_id == 151656
@@ -75,6 +85,16 @@ def test_qwen3_omni_custom_thinker_config(self):
7585

7686
assert provider.thinker_config.text_config.hidden_size == 128
7787

88+
def test_qwen3_omni_uses_mcore_routing_replay_field(self):
89+
provider = Qwen3OmniModelProvider(
90+
num_layers=2,
91+
hidden_size=128,
92+
num_attention_heads=4,
93+
moe_enable_routing_replay=True,
94+
)
95+
96+
assert provider.moe_enable_routing_replay is True
97+
7898
def test_qwen3_omni_freeze_flags(self):
7999
provider = Qwen3OmniModelProvider(
80100
num_layers=48,
@@ -181,6 +201,51 @@ def test_default_backend_keeps_te_core_attention_when_available(self):
181201
assert self_attention_spec.module is Qwen3VLSelfAttention
182202
assert self_attention_spec.submodules.core_attention.__name__ == "TEDotProductAttention"
183203

204+
@pytest.mark.parametrize("attention_backend", [AttnBackend.unfused, "unfused"])
205+
def test_unfused_backend_keeps_te_core_attention_with_sequence_parallel(self, attention_backend):
206+
provider = Qwen3OmniModelProvider(
207+
num_layers=2,
208+
hidden_size=128,
209+
ffn_hidden_size=256,
210+
num_attention_heads=4,
211+
num_query_groups=2,
212+
kv_channels=32,
213+
vocab_size=1024,
214+
use_cpu_initialization=True,
215+
bf16=False,
216+
attention_backend=attention_backend,
217+
sequence_parallel=True,
218+
)
219+
220+
with (
221+
patch("megatron.bridge.models.qwen_omni.qwen3_omni_provider.HAVE_TE", True),
222+
patch("megatron.bridge.models.qwen_omni.qwen3_omni_provider.Qwen3OmniModel") as mock_model_cls,
223+
):
224+
provider.provide()
225+
226+
_, kwargs = mock_model_cls.call_args
227+
self_attention_spec = kwargs["language_transformer_layer_spec"].submodules.self_attention
228+
assert self_attention_spec.module is Qwen3VLSelfAttention
229+
assert self_attention_spec.submodules.core_attention.__name__ == "TEDotProductAttention"
230+
231+
def test_local_backend_with_sequence_parallel_hard_fails(self):
232+
provider = Qwen3OmniModelProvider(
233+
num_layers=2,
234+
hidden_size=128,
235+
ffn_hidden_size=256,
236+
num_attention_heads=4,
237+
num_query_groups=2,
238+
kv_channels=32,
239+
vocab_size=1024,
240+
use_cpu_initialization=True,
241+
bf16=False,
242+
attention_backend="local",
243+
sequence_parallel=True,
244+
)
245+
246+
with pytest.raises(ValueError, match="sequence_parallel requires a Transformer Engine attention backend"):
247+
provider.provide()
248+
184249
def test_default_backend_falls_back_to_local_spec_without_te(self):
185250
provider = Qwen3OmniModelProvider(
186251
num_layers=2,

0 commit comments

Comments
 (0)