diff --git a/README.md b/README.md
index 233f8ed..175afdc 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ The following is the list of models supported by MCore-Bridge:
| Series | model_type |
| -------- | ------------------------------------------------------------ |
-| Qwen | qwen2, qwen2_moe
qwen2_vl, qwen2_5_vl, qwen2_5_omni
qwen3, qwen3_moe
qwen3_vl, qwen3_vl_moe, qwen3_omni_moe
qwen3_next, qwen3_5, qwen3_5_moe |
+| Qwen | qwen2, qwen2_moe
qwen2_vl, qwen2_5_vl, qwen2_5_omni
qwen3, qwen3_moe
qwen3_vl, qwen3_vl_moe, qwen3_omni_moe, qwen3_asr
qwen3_next, qwen3_5, qwen3_5_moe |
| DeepSeek | deepseek_v3, deepseek_v32 |
| GLM | glm4, glm4_moe, glm4_moe_lite
glm4v, glm4v_moe,
glm_moe_dsa |
| MiniMax | minimax_m2 |
diff --git a/README_zh.md b/README_zh.md
index 3518b73..d5e9d55 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -80,7 +80,7 @@ uv pip install -e . --torch-backend=auto
| 系列 | model_type |
| -------- | ------------------------------------------------------------ |
-| Qwen | qwen2, qwen2_moe
qwen2_vl, qwen2_5_vl, qwen2_5_omni
qwen3, qwen3_moe
qwen3_vl, qwen3_vl_moe, qwen3_omni_moe
qwen3_next, qwen3_5, qwen3_5_moe |
+| Qwen | qwen2, qwen2_moe
qwen2_vl, qwen2_5_vl, qwen2_5_omni
qwen3, qwen3_moe
qwen3_vl, qwen3_vl_moe, qwen3_omni_moe, qwen3_asr
qwen3_next, qwen3_5, qwen3_5_moe |
| DeepSeek | deepseek_v3, deepseek_v32 |
| GLM | glm4, glm4_moe, glm4_moe_lite
glm4v, glm4v_moe,
glm_moe_dsa |
| MiniMax | minimax_m2 |
diff --git a/src/mcore_bridge/config/model_config.py b/src/mcore_bridge/config/model_config.py
index 943c100..8b5438a 100644
--- a/src/mcore_bridge/config/model_config.py
+++ b/src/mcore_bridge/config/model_config.py
@@ -321,7 +321,10 @@ def __post_init__(self):
self.mcore_model_type = get_mcore_model_type(self.hf_model_type)
self.model_meta = get_model_meta(self.mcore_model_type)
self.is_multimodal = self.model_meta.visual_cls is not None
- self.support_multimodal = self.is_multimodal and getattr(self.model_meta.visual_cls, 'support_multimodal', True)
+ if self.is_multimodal:
+ self.test_mm_type = getattr(self.model_meta.visual_cls, 'test_mm_type', 'image')
+ else:
+ self.test_mm_type = 'text'
if self.is_multimodal and self.hf_config is None:
raise ValueError('Multimodal model must specify hf_config.')
self.is_moe_model = self.num_moe_experts is not None
diff --git a/src/mcore_bridge/model/constant.py b/src/mcore_bridge/model/constant.py
index ffbeaba..58b0ee3 100644
--- a/src/mcore_bridge/model/constant.py
+++ b/src/mcore_bridge/model/constant.py
@@ -19,6 +19,7 @@ class MLLMModelType:
qwen3_vl = 'qwen3_vl'
qwen2_5_omni = 'qwen2_5_omni'
qwen3_omni = 'qwen3_omni'
+ qwen3_asr = 'qwen3_asr'
qwen3_5 = 'qwen3_5'
ovis2_5 = 'ovis2_5'
diff --git a/src/mcore_bridge/model/gpt_model.py b/src/mcore_bridge/model/gpt_model.py
index e78ffd5..31416d5 100644
--- a/src/mcore_bridge/model/gpt_model.py
+++ b/src/mcore_bridge/model/gpt_model.py
@@ -294,7 +294,8 @@ def forward(
runtime_gather_output (bool): Gather output at runtime. Default None means
`parallel_output` arg in the constructor will be used.
"""
-
+ if self.config.position_embedding_type == 'mrope' and position_ids.ndim == 2: # qwen3_asr
+ position_ids = position_ids.unsqueeze(0).expand(3, -1, -1)
inference_context = deprecate_inference_params(inference_context, inference_params)
decoder_input, rotary_pos_emb, rotary_pos_cos, rotary_pos_sin, sequence_len_offset = (
diff --git a/src/mcore_bridge/model/mm_gpts/__init__.py b/src/mcore_bridge/model/mm_gpts/__init__.py
index d13e4e7..9009edb 100644
--- a/src/mcore_bridge/model/mm_gpts/__init__.py
+++ b/src/mcore_bridge/model/mm_gpts/__init__.py
@@ -1,2 +1,2 @@
# Copyright (c) ModelScope Contributors. All rights reserved.
-from . import glm, internvl, kimi_vl, llama4, llava, qwen, qwen3_5, qwen3_5_gdn, qwen3_omni, qwen3_vl
+from . import glm, internvl, kimi_vl, llama4, llava, qwen, qwen3_5, qwen3_5_gdn, qwen3_asr, qwen3_omni, qwen3_vl
diff --git a/src/mcore_bridge/model/mm_gpts/kimi_vl.py b/src/mcore_bridge/model/mm_gpts/kimi_vl.py
index a4bc6ad..df8a7cb 100644
--- a/src/mcore_bridge/model/mm_gpts/kimi_vl.py
+++ b/src/mcore_bridge/model/mm_gpts/kimi_vl.py
@@ -72,7 +72,7 @@ class KimiK25Vit(HuggingFaceVit):
module_mapping = {'vision_tower': 'vision_tower', 'mm_projector': 'mm_projector'}
_vision_tower = ['vision_tower']
_aligner = ['mm_projector']
- support_multimodal = False
+ test_mm_type = 'text'
def prepare_model(self, hf_config: PretrainedConfig):
output = []
diff --git a/src/mcore_bridge/model/mm_gpts/qwen3_asr.py b/src/mcore_bridge/model/mm_gpts/qwen3_asr.py
new file mode 100644
index 0000000..8b58908
--- /dev/null
+++ b/src/mcore_bridge/model/mm_gpts/qwen3_asr.py
@@ -0,0 +1,60 @@
+# Copyright (c) ModelScope Contributors. All rights reserved.
+import torch
+from transformers import PretrainedConfig
+
+from mcore_bridge.bridge import MultimodalGPTBridge
+
+from ..constant import ModelType
+from ..register import ModelMeta, register_model
+from .utils import HuggingFaceVit
+
+
+class Qwen3ASRBridge(MultimodalGPTBridge):
+ hf_layers_prefix = 'thinker.model.layers'
+ hf_embed_key = 'thinker.model.embed_tokens.weight'
+ hf_final_layernorm_key = 'thinker.model.norm.weight'
+ hf_lm_head_key = 'thinker.lm_head.weight'
+ hf_score_key = 'thinker.score.weight'
+
+
+class Qwen3ASRVit(HuggingFaceVit):
+ module_mapping = {'thinker.audio_tower': 'audio_tower'}
+ _vision_tower = ['audio_tower']
+ _aligner = ['audio_tower.proj1', 'audio_tower.proj2']
+ test_mm_type = 'audio'
+
+ def prepare_model(self, hf_config: PretrainedConfig):
+ from qwen_asr.core.transformers_backend.modeling_qwen3_asr import (Qwen3ASRAudioEncoder,
+ Qwen3ASRThinkerForConditionalGeneration)
+ self.audio_tower = Qwen3ASRAudioEncoder._from_config(hf_config.thinker_config.audio_config)
+ self.model_cls = Qwen3ASRThinkerForConditionalGeneration
+
+ def get_inputs_embeds(self, inputs_embeds, **kwargs):
+ input_ids = kwargs['input_ids']
+ hf_config = self.hf_config.thinker_config
+ input_features = kwargs.get('input_features')
+ feature_attention_mask = kwargs.get('feature_attention_mask')
+
+ if input_features is None:
+ input_features = input_ids.new_zeros([1, 128, 128], dtype=self.audio_tower.dtype)
+ feature_attention_mask = input_ids.new_ones([1, 128], dtype=torch.bool)
+ audio_embeds = self.get_audio_features(input_features, feature_attention_mask)
+ inputs_embeds = inputs_embeds + audio_embeds.mean() * 0.
+ else:
+ audio_embeds = self.get_audio_features(input_features, feature_attention_mask)
+ audio_mask = (input_ids == hf_config.audio_token_id).unsqueeze(-1).expand_as(inputs_embeds)
+ audio_embeds = audio_embeds.to(inputs_embeds.device, inputs_embeds.dtype)
+ inputs_embeds = inputs_embeds.masked_scatter(audio_mask, audio_embeds)
+ return inputs_embeds
+
+ def get_audio_features(self, *args, **kwargs):
+ with self.patch_hf_config():
+ return self.model_cls.get_audio_features(self, *args, **kwargs)
+
+
+register_model(ModelMeta(
+ ModelType.qwen3_asr,
+ ['qwen3_asr'],
+ bridge_cls=Qwen3ASRBridge,
+ visual_cls=Qwen3ASRVit,
+))
diff --git a/src/mcore_bridge/model/mm_gpts/utils.py b/src/mcore_bridge/model/mm_gpts/utils.py
index cf778c4..f333ced 100644
--- a/src/mcore_bridge/model/mm_gpts/utils.py
+++ b/src/mcore_bridge/model/mm_gpts/utils.py
@@ -26,7 +26,7 @@ def new_get_cached_module_file(pretrained_model_name_or_path, *args, **kwargs):
class HuggingFaceVit(_HuggingFaceModule, ABC):
module_mapping = {} # hf -> mcore
- support_multimodal = True
+ test_mm_type = 'image'
@contextmanager
def patch_hf_config(self):
diff --git a/tests/test_mllm.py b/tests/test_mllm.py
index f4832f7..1fe103e 100644
--- a/tests/test_mllm.py
+++ b/tests/test_mllm.py
@@ -112,6 +112,10 @@ def test_llava_onevision1_5():
_test_model('lmms-lab/LLaVA-OneVision-1.5-4B-Instruct')
+def test_qwen3_asr():
+ _test_model('Qwen/Qwen3-ASR-1.7B')
+
+
if __name__ == '__main__':
# test_qwen2_5_vl()
# test_qwen2_vl()
@@ -131,4 +135,5 @@ def test_llava_onevision1_5():
# test_qwen3_omni()
# test_llama4()
# test_qwen3_5()
- test_llava_onevision1_5()
+ # test_llava_onevision1_5()
+ test_qwen3_asr()