Skip to content

Commit d36160c

Browse files
committed
feat: Add Qwen3-VL-2B config and E2E tests
1 parent 571d9ed commit d36160c

9 files changed

Lines changed: 129 additions & 9 deletions

File tree

src/maxtext/checkpoint_conversion/utils/hf_model_configs.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,6 +1642,62 @@ def __init__(self, **kwargs):
16421642
}
16431643
qwen3_vl_4b_config = PTConfig(**qwen3_vl_4b_dict)
16441644

1645+
qwen3_vl_2b_dict = {
1646+
"architectures": ["Qwen3VLForConditionalGeneration"],
1647+
"image_token_id": 151655,
1648+
"model_type": "qwen3_vl",
1649+
"text_config": {
1650+
"attention_bias": False,
1651+
"attention_dropout": 0.0,
1652+
"bos_token_id": 151643,
1653+
"dtype": "bfloat16",
1654+
"eos_token_id": 151645,
1655+
"head_dim": 128,
1656+
"hidden_act": "silu",
1657+
"hidden_size": 2048,
1658+
"initializer_range": 0.02,
1659+
"intermediate_size": 6144,
1660+
"max_position_embeddings": 262144,
1661+
"model_type": "qwen3_vl_text",
1662+
"num_attention_heads": 16,
1663+
"num_hidden_layers": 28,
1664+
"num_key_value_heads": 8,
1665+
"pad_token_id": None,
1666+
"rms_norm_eps": 1e-06,
1667+
"rope_parameters": {
1668+
"mrope_interleaved": True,
1669+
"mrope_section": [24, 20, 20],
1670+
"rope_theta": 5000000,
1671+
"rope_type": "default",
1672+
},
1673+
"tie_word_embeddings": True,
1674+
"use_cache": True,
1675+
"vocab_size": 151936,
1676+
},
1677+
"tie_word_embeddings": True,
1678+
"transformers_version": "4.57.0.dev0",
1679+
"video_token_id": 151656,
1680+
"vision_config": {
1681+
"deepstack_visual_indexes": [5, 11, 17],
1682+
"depth": 24,
1683+
"hidden_act": "gelu_pytorch_tanh",
1684+
"hidden_size": 1024,
1685+
"in_channels": 3,
1686+
"initializer_range": 0.02,
1687+
"intermediate_size": 4096,
1688+
"model_type": "qwen3_vl_vision",
1689+
"num_heads": 16,
1690+
"num_position_embeddings": 2304,
1691+
"out_hidden_size": 2048,
1692+
"patch_size": 16,
1693+
"spatial_merge_size": 2,
1694+
"temporal_patch_size": 2,
1695+
},
1696+
"vision_end_token_id": 151653,
1697+
"vision_start_token_id": 151652,
1698+
}
1699+
qwen3_vl_2b_config = PTConfig(**qwen3_vl_2b_dict)
1700+
16451701

16461702
# {maxtext model name: hf model config}
16471703
HF_MODEL_CONFIGS = {
@@ -1669,6 +1725,7 @@ def __init__(self, **kwargs):
16691725
"qwen3-14b": qwen3_14b_config,
16701726
"qwen3-14b-base": qwen3_14b_config,
16711727
"qwen3-32b": qwen3_32b_config,
1728+
"qwen3-vl-2b": qwen3_vl_2b_config,
16721729
"qwen3-vl-4b": qwen3_vl_4b_config,
16731730
"llama3.1-8b": llama31_8b_config,
16741731
"llama3.1-8b-Instruct": llama31_8b_config,

src/maxtext/checkpoint_conversion/utils/hf_shape.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,7 @@ def QWEN3_VL_HF_WEIGHTS_TO_SHAPE(config):
11821182
"qwen3-8b": QWEN_HF_WEIGHTS_TO_SHAPE,
11831183
"qwen3-14b": QWEN_HF_WEIGHTS_TO_SHAPE,
11841184
"qwen3-32b": QWEN_HF_WEIGHTS_TO_SHAPE,
1185+
"qwen3-vl-2b": QWEN3_VL_HF_WEIGHTS_TO_SHAPE,
11851186
"qwen3-vl-4b": QWEN3_VL_HF_WEIGHTS_TO_SHAPE,
11861187
"llama3.1-8b": LLAMA31_HF_WEIGHTS_TO_SHAPE,
11871188
"llama3.1-8b-Instruct": LLAMA31_HF_WEIGHTS_TO_SHAPE,

src/maxtext/checkpoint_conversion/utils/param_mapping.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3883,6 +3883,7 @@ def reshape_vision_attn_out(input_tensor, target_shape):
38833883
"qwen3-14b": QWEN_MAXTEXT_TO_HF_PARAM_MAPPING,
38843884
"qwen3-14b-base": QWEN_MAXTEXT_TO_HF_PARAM_MAPPING,
38853885
"qwen3-32b": QWEN_MAXTEXT_TO_HF_PARAM_MAPPING,
3886+
"qwen3-vl-2b": QWEN3_VL_MAXTEXT_TO_HF_PARAM_MAPPING,
38863887
"qwen3-vl-4b": QWEN3_VL_MAXTEXT_TO_HF_PARAM_MAPPING,
38873888
"llama3.1-8b": LLAMA31_MAXTEXT_TO_HF_PARAM_MAPPING,
38883889
"llama3.1-8b-Instruct": LLAMA31_MAXTEXT_TO_HF_PARAM_MAPPING,
@@ -3934,6 +3935,7 @@ def reshape_vision_attn_out(input_tensor, target_shape):
39343935
"qwen3-14b": QWEN_MAXTEXT_TO_HF_PARAM_HOOK_FN,
39353936
"qwen3-14b-base": QWEN_MAXTEXT_TO_HF_PARAM_HOOK_FN,
39363937
"qwen3-32b": QWEN_MAXTEXT_TO_HF_PARAM_HOOK_FN,
3938+
"qwen3-vl-2b": QWEN3_VL_MAXTEXT_TO_HF_PARAM_HOOK_FN,
39373939
"qwen3-vl-4b": QWEN3_VL_MAXTEXT_TO_HF_PARAM_HOOK_FN,
39383940
"llama3.1-8b": LLAMA31_MAXTEXT_TO_HF_PARAM_HOOK_FN,
39393941
"llama3.1-8b-Instruct": LLAMA31_MAXTEXT_TO_HF_PARAM_HOOK_FN,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Model config for Qwen/Qwen3-VL-2B-Instruct
16+
17+
# Core Architectural Parameters
18+
decoder_block: "qwen3"
19+
base_emb_dim: 2048
20+
base_mlp_dim: 6144
21+
base_num_query_heads: 16
22+
base_num_kv_heads: 8
23+
base_num_decoder_layers: 28
24+
head_dim: 128
25+
mlp_activations: ["silu", "linear"]
26+
vocab_size: 151936
27+
normalization_layer_epsilon: 1.0e-6
28+
use_qk_norm: true
29+
logits_via_embedding: true
30+
normalize_embedding_logits: false
31+
32+
# RoPE Settings
33+
rope_max_timescale: 5000000
34+
35+
# General Model Settings
36+
enable_dropout: false
37+
38+
# Vision Encoder Configuration
39+
# Based on HuggingFace AutoConfig for Qwen/Qwen3-VL-2B-Instruct
40+
use_multimodal: true
41+
image_size_for_vit: 768
42+
hidden_size_for_vit: 1024
43+
intermediate_size_for_vit: 4096
44+
num_attention_heads_for_vit: 16
45+
num_hidden_layers_for_vit: 24
46+
num_channels_for_vit: 3
47+
patch_size_for_vit: 16
48+
temporal_patch_size_for_vit: 2
49+
spatial_merge_size_for_vit: 2
50+
out_hidden_size_for_vit: 2048
51+
num_position_embeddings_for_vit: 2304
52+
deepstack_visual_indexes_for_vit: [5, 11, 17]
53+
54+
# MRoPE Settings
55+
use_mrope: true
56+
mrope_section: [24, 20, 20]

src/maxtext/configs/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ class ProfilerType(str, Enum):
261261
"qwen3-30b-a3b",
262262
"qwen3-30b-a3b-base",
263263
"qwen3-480b-a35b",
264+
"qwen3-vl-2b",
264265
"qwen3-vl-4b",
265266
"qwen3-next-80b-a3b",
266267
"qwen3-omni-30b-a3b",
@@ -3156,6 +3157,7 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
31563157
"llama4-17b-16e",
31573158
"llama4-17b-128e",
31583159
"qwen3-omni-30b-a3b",
3160+
"qwen3-vl-2b",
31593161
"qwen3-vl-4b",
31603162
"qwen3.5-35b-a3b",
31613163
"qwen3.5-397b-a17b",

src/maxtext/layers/decoders.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,7 @@ def _apply_embedding(
728728
"llama4-17b-16e",
729729
"llama4-17b-128e",
730730
"qwen3-omni-30b-a3b",
731+
"qwen3-vl-2b",
731732
"qwen3-vl-4b",
732733
"qwen3.5-35b-a3b",
733734
"qwen3.5-397b-a17b",
@@ -743,7 +744,7 @@ def _apply_embedding(
743744
raise ValueError(f"Unsupported model_name for multimodal: {cfg.model_name}")
744745

745746
if video_embeddings is not None and cfg.use_multimodal:
746-
if cfg.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
747+
if cfg.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-2b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
747748
y = mm_utils.merge_mm_embeddings(
748749
text_embeddings=y,
749750
multimodal_embeddings=video_embeddings,

src/maxtext/layers/encoders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def _setup_vision_encoder_layers(self):
8080
)
8181
setattr(self, projector_name, qwen3_5_vision.Qwen3_5MoeVisionProjector(config=self.config, rngs=self.rngs))
8282
return encoder_name, projector_name
83-
elif self.config.model_name in ["qwen3-vl-4b"]:
83+
elif self.config.model_name in ["qwen3-vl-4b", "qwen3-vl-2b"]:
8484
from maxtext.models import qwen3_vl_vision # pylint: disable=import-outside-toplevel
8585

8686
encoder_name = "Qwen3VLVisionEncoder_0"

src/maxtext/multimodal/processor.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def preprocess_mm_data(config):
4444

4545
images = [mm_utils.load_image_from_path(p) for p in config.image_path.split(",")]
4646
processor_outputs = preprocess_mm_data_llama4(images)
47-
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
47+
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-2b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
4848
from maxtext.multimodal.processor_qwen3_omni import preprocess_mm_data_qwen3_omni # pylint: disable=import-outside-toplevel
4949

5050
processor_outputs = preprocess_mm_data_qwen3_omni(config)
@@ -68,7 +68,7 @@ def preprocess_image_for_training(image, config):
6868
from maxtext.multimodal.processor_llama4 import preprocess_mm_data_llama4 # pylint: disable=import-outside-toplevel
6969

7070
return preprocess_mm_data_llama4(image)
71-
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
71+
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-2b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
7272
from maxtext.multimodal.processor_qwen3_omni import preprocess_mm_data_qwen3_omni_for_training # pylint: disable=import-outside-toplevel
7373

7474
return preprocess_mm_data_qwen3_omni_for_training(image, config)
@@ -90,7 +90,7 @@ def get_image_offsets(config, processor_output: mm_utils.PreprocessorOutput | No
9090
from maxtext.multimodal.processor_llama4 import get_image_offsets_llama4 # pylint: disable=import-outside-toplevel
9191

9292
return get_image_offsets_llama4(processor_output)
93-
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
93+
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-2b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
9494
from maxtext.multimodal.processor_qwen3_omni import get_mm_offsets_qwen3_omni # pylint: disable=import-outside-toplevel
9595

9696
return get_mm_offsets_qwen3_omni(config, processor_output)
@@ -112,7 +112,7 @@ def reformat_prompt(prompt, image_placeholder, model_name, num_images, video_pla
112112
from maxtext.multimodal.processor_llama4 import reformat_prompt_llama4 # pylint: disable=import-outside-toplevel
113113

114114
return reformat_prompt_llama4(prompt, image_placeholder, num_images)
115-
elif model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
115+
elif model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-2b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
116116
from maxtext.multimodal.processor_qwen3_omni import reformat_prompt_qwen3_omni # pylint: disable=import-outside-toplevel
117117

118118
return reformat_prompt_qwen3_omni(
@@ -137,7 +137,7 @@ def reformat_response(response, model_name):
137137
elif model_name in ["gemma4-26b", "gemma4-31b", "gemma4-e2b", "gemma4-e4b"]:
138138
formatted_response = f"{response}<turn|>"
139139
return formatted_response
140-
elif model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
140+
elif model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-2b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
141141
formatted_response = f"{response}<|im_end|>"
142142
return formatted_response
143143
else:
@@ -158,7 +158,7 @@ def prepare_text_for_image_fusion(tokens, config, processor_output=None):
158158
from maxtext.multimodal.processor_llama4 import add_extra_tokens_for_images_llama4 # pylint: disable=import-outside-toplevel
159159

160160
return add_extra_tokens_for_images_llama4(tokens, processor_output)
161-
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
161+
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-2b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
162162
from maxtext.multimodal.processor_qwen3_omni import add_extra_tokens_for_qwen3_omni # pylint: disable=import-outside-toplevel
163163

164164
return add_extra_tokens_for_qwen3_omni(tokens, config, processor_output)
@@ -222,7 +222,7 @@ def get_bidirectional_mask_vision(config, decoder_input_tokens, is_video: bool =
222222
from maxtext.multimodal.processor_llama4 import LLAMA4_PATCH_TOKEN # pylint: disable=import-outside-toplevel
223223

224224
bidirectional_mask_vision = decoder_input_tokens == LLAMA4_PATCH_TOKEN
225-
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
225+
elif config.model_name in ["qwen3-omni-30b-a3b", "qwen3-vl-2b", "qwen3-vl-4b", "qwen3.5-35b-a3b", "qwen3.5-397b-a17b"]:
226226
from maxtext.multimodal.processor_qwen3_omni import QwenTokens # pylint: disable=import-outside-toplevel
227227

228228
tokens = QwenTokens(config)

src/maxtext/utils/globals.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"qwen3-8b": "Qwen/Qwen3-8B",
6464
"qwen3-14b": "Qwen/Qwen3-14B",
6565
"qwen3-32b": "Qwen/Qwen3-32B",
66+
"qwen3-vl-2b": "Qwen/Qwen3-VL-2B-Instruct",
6667
"qwen3-vl-4b": "Qwen/Qwen3-VL-4B-Instruct",
6768
"llama3.1-8b": "meta-llama/Llama-3.1-8B",
6869
"llama3.1-8b-Instruct": "meta-llama/Llama-3.1-8B-Instruct",

0 commit comments

Comments
 (0)