[Model] Add Bailing V3 VL support (#55921)
Signed-off-by: zexplorerhj <zhjoneson@163.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
zexplorerhj committed
Sep 10, 2026 at 17:46 UTC
fe26c705f7099da3b155b3cb6218f013bb308e63
19 files changed
+1825
-20
docs/models/supported_models.md
+1
@@ -538,6 +538,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
538
| `AriaForConditionalGeneration` | Aria | T + I<sup>+</sup> | `rhymes-ai/Aria` | | |
539
| `AudioFlamingo3ForConditionalGeneration` | AudioFlamingo3 | T + A | `nvidia/audio-flamingo-3-hf`, `nvidia/music-flamingo-hf` | ✅︎ | ✅︎ |
540
| `BagelForConditionalGeneration` | BAGEL | T + I<sup>+</sup> | `ByteDance-Seed/BAGEL-7B-MoT` | ✅︎ | ✅︎ |
541
+| `BailingMoeV3VLForConditionalGeneration` | Ling 3.0 Flash VL | T + I<sup>E+</sup> | `inclusionAI/Ling-3.0-flash-VL` | | ✅︎ |
542
| `BeeForConditionalGeneration` | Bee-8B | T + I<sup>E+</sup> | `Open-Bee/Bee-8B-RL`, `Open-Bee/Bee-8B-SFT` | | ✅︎ |
543
| `Blip2ForConditionalGeneration` | BLIP-2 | T + I<sup>E</sup> | `Salesforce/blip2-opt-2.7b`, `Salesforce/blip2-opt-6.7b`, etc. | ✅︎ | ✅︎ |
544
| `Cohere2VisionForConditionalGeneration` | Command A Vision, Command-A+ | T + I<sup>+</sup> | `CohereLabs/command-a-vision-07-2025`, `CohereLabs/command-a-plus-05-2026`, etc. | | ✅︎ |
tests/model_executor/test_bailing_mrope.py
new
+176
@@ -0,0 +1,176 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import copy
5
+from types import SimpleNamespace
6
+
7
+import pytest
8
+import torch
9
+
10
+from vllm.config import DeviceConfig, VllmConfig, set_current_vllm_config
11
+from vllm.model_executor.layers.rotary_embedding import get_rope
12
+from vllm.model_executor.layers.rotary_embedding.bailing_mrope import (
13
+ BailingMRotaryEmbedding,
14
+)
15
+from vllm.model_executor.models.bailing_moe_v3 import _build_mla_rotary_embedding
16
+from vllm.platforms import current_platform
17
+
18
+
19
+def test_bailing_mrope_selects_alternating_spatial_then_temporal():
20
+ with set_current_vllm_config(VllmConfig(device_config=DeviceConfig("cpu"))):
21
+ rotary_emb = BailingMRotaryEmbedding(
22
+ head_size=12,
23
+ rotary_dim=12,
24
+ max_position_embeddings=8,
25
+ base=10_000,
26
+ is_neox_style=False,
27
+ dtype=torch.float32,
28
+ mrope_section=[2, 2, 2],
29
+ )
30
+
31
+ positions = torch.tensor([[1, 2], [3, 4], [5, 6]])
32
+ frequencies = torch.arange(6).expand(8, -1)
33
+ position_values = torch.arange(8).unsqueeze(1) * 10
34
+ cos = position_values + frequencies
35
+ sin = cos + 100
36
+ cos_sin_cache = torch.cat((cos, sin), dim=-1)
37
+
38
+ actual_cos, actual_sin = rotary_emb.select_cos_sin(
39
+ positions,
40
+ cos_sin_cache,
41
+ )
42
+
43
+ expected_cos = torch.tensor(
44
+ [
45
+ [30, 51, 32, 53, 14, 15],
46
+ [40, 61, 42, 63, 24, 25],
47
+ ]
48
+ )
49
+ torch.testing.assert_close(actual_cos, expected_cos)
50
+ torch.testing.assert_close(actual_sin, expected_cos + 100)
51
+
52
+
53
+def test_bailing_factory_preserves_mla_width_and_config():
54
+ config = SimpleNamespace(
55
+ max_position_embeddings=128,
56
+ rope_parameters={
57
+ "rope_type": "default",
58
+ "rope_theta": 6_000_000,
59
+ "partial_rotary_factor": 0.5,
60
+ "mrope_section": [8, 12, 12],
61
+ },
62
+ rope_scaling={"partial_rotary_factor": 0.5},
63
+ )
64
+ original = copy.deepcopy(vars(config))
65
+ with set_current_vllm_config(VllmConfig(device_config=DeviceConfig("cpu"))):
66
+ rope = _build_mla_rotary_embedding(config, head_size=64)
67
+ assert rope is _build_mla_rotary_embedding(config, head_size=64)
68
+ generic = get_rope(
69
+ head_size=64,
70
+ max_position=128,
71
+ is_neox_style=False,
72
+ rope_parameters={
73
+ "rope_type": "default",
74
+ "rope_theta": 6_000_000,
75
+ "mrope_section": [8, 12, 12],
76
+ },
77
+ )
78
+ assert isinstance(rope, BailingMRotaryEmbedding)
79
+ assert rope is not generic
80
+ assert rope.rotary_dim == 64
81
+ assert vars(config) == original
82
+
83
+
84
+def test_bailing_factory_rejects_unsupported_scaling():
85
+ config = SimpleNamespace(
86
+ rope_parameters={
87
+ "rope_type": "yarn",
88
+ "factor": 2.0,
89
+ "mrope_section": [8, 12, 12],
90
+ }
91
+ )
92
+ with pytest.raises(ValueError, match="only supports rope_type='default'"):
93
+ _build_mla_rotary_embedding(config, head_size=64)
94
+
95
+
96
+@pytest.mark.skipif(
97
+ not current_platform.is_cuda_alike(),
98
+ reason="Skipping CUDA/ROCm-only test.",
99
+)
100
+@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
101
+@pytest.mark.parametrize("position_kind", ["text", "multimodal"])
102
+@torch.inference_mode()
103
+def test_bailing_mrope_cuda_matches_native_for_strided_mla_views(
104
+ dtype: torch.dtype,
105
+ position_kind: str,
106
+):
107
+ device = torch.device("cuda")
108
+ num_tokens = 17
109
+ num_query_heads = 8
110
+ with set_current_vllm_config(VllmConfig()):
111
+ rotary_emb = BailingMRotaryEmbedding(
112
+ head_size=64,
113
+ rotary_dim=64,
114
+ max_position_embeddings=64,
115
+ base=6_000_000,
116
+ is_neox_style=False,
117
+ dtype=dtype,
118
+ mrope_section=[8, 12, 12],
119
+ ).to(device)
120
+
121
+ torch.manual_seed(0)
122
+ multimodal_positions = torch.randint(
123
+ 0,
124
+ 64,
125
+ (3, 2 * num_tokens),
126
+ dtype=torch.int32,
127
+ device=device,
128
+ )[:, ::2]
129
+ if position_kind == "multimodal":
130
+ positions = multimodal_positions
131
+ reference_positions = positions
132
+ else:
133
+ positions = torch.randint(
134
+ 0,
135
+ 64,
136
+ (2 * num_tokens,),
137
+ dtype=torch.int64,
138
+ device=device,
139
+ )[::2]
140
+ reference_positions = positions.expand(3, -1)
141
+ query_buffer = torch.randn(
142
+ num_tokens,
143
+ num_query_heads,
144
+ 192,
145
+ dtype=dtype,
146
+ device=device,
147
+ )
148
+ key_buffer = torch.randn(
149
+ num_tokens,
150
+ 576,
151
+ dtype=dtype,
152
+ device=device,
153
+ )
154
+ query = query_buffer[..., 128:]
155
+ key = key_buffer[:, 512:].unsqueeze(1)
156
+ assert not query.is_contiguous()
157
+ assert not key.is_contiguous()
158
+
159
+ query_prefix = query_buffer[..., :128].clone()
160
+ key_prefix = key_buffer[:, :512].clone()
161
+ expected_query, expected_key = rotary_emb.forward_native(
162
+ reference_positions,
163
+ query.clone(),
164
+ key.clone(),
165
+ )
166
+ actual_query, actual_key = rotary_emb.forward_cuda(positions, query, key)
167
+ assert expected_key is not None
168
+ assert actual_key is not None
169
+
170
+ torch.testing.assert_close(actual_query, expected_query, atol=1e-2, rtol=1.6e-2)
171
+ torch.testing.assert_close(actual_key, expected_key, atol=1e-2, rtol=1.6e-2)
172
+ assert torch.equal(query_buffer[..., :128], query_prefix)
173
+ assert torch.equal(key_buffer[:, :512], key_prefix)
174
+ if position_kind == "multimodal":
175
+ assert actual_query.data_ptr() == query.data_ptr()
176
+ assert actual_key.data_ptr() == key.data_ptr()
tests/models/multimodal/processing/test_bailing_moe_v3_vl.py
new
+126
@@ -0,0 +1,126 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Bailing prompt expansion tests without downloading a checkpoint."""
4
+
5
+from types import SimpleNamespace
6
+
7
+import pytest
8
+import torch
9
+from PIL import Image
10
+from tokenizers import Tokenizer, decoders, models, pre_tokenizers
11
+from transformers import PreTrainedTokenizerFast
12
+
13
+from vllm.model_executor.models.bailing_moe_v3_vl import (
14
+ IMAGE_PLACEHOLDER,
15
+ IMAGE_TOKEN,
16
+ BailingMoeV3VLMultiModalProcessor,
17
+)
18
+from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems
19
+from vllm.multimodal.parse import ImageProcessorItems, MultiModalDataItems
20
+
21
+pytestmark = pytest.mark.cpu_test
22
+
23
+
24
+@pytest.fixture
25
+def processor():
26
+ vocab = {
27
+ token: index
28
+ for index, token in enumerate(sorted(pre_tokenizers.ByteLevel.alphabet()))
29
+ }
30
+ # A newline added by the processor must merge with an existing newline.
31
+ vocab["ĊĊ"] = len(vocab)
32
+ backend = Tokenizer(models.BPE(vocab=vocab, merges=[("Ċ", "Ċ")]))
33
+ backend.pre_tokenizer = pre_tokenizers.ByteLevel(
34
+ add_prefix_space=False, use_regex=False
35
+ )
36
+ backend.decoder = decoders.ByteLevel()
37
+ tokenizer = PreTrainedTokenizerFast(
38
+ tokenizer_object=backend,
39
+ additional_special_tokens=[
40
+ "<|vision_start|>",
41
+ IMAGE_TOKEN,
42
+ "<|vision_end|>",
43
+ ],
44
+ clean_up_tokenization_spaces=False,
45
+ )
46
+ processor = object.__new__(BailingMoeV3VLMultiModalProcessor)
47
+ processor.info = SimpleNamespace(
48
+ get_tokenizer=lambda: tokenizer,
49
+ get_hf_config=lambda: SimpleNamespace(
50
+ vision_config=SimpleNamespace(spatial_merge_size=2)
51
+ ),
52
+ )
53
+ return processor
54
+
55
+
56
+@pytest.mark.parametrize(
57
+ ("prompt", "expected_text", "token_counts"),
58
+ [
59
+ (
60
+ f"before{IMAGE_PLACEHOLDER}after",
61
+ f"before<|vision_start|>{IMAGE_TOKEN * 2}<|vision_end|>\nafter",
62
+ [2],
63
+ ),
64
+ (
65
+ f"before{IMAGE_TOKEN}after",
66
+ f"before<|vision_start|>{IMAGE_TOKEN * 2}<|vision_end|>\nafter",
67
+ [2],
68
+ ),
69
+ (
70
+ f"before{IMAGE_PLACEHOLDER}\nafter",
71
+ f"before<|vision_start|>{IMAGE_TOKEN * 2}<|vision_end|>\n\nafter",
72
+ [2],
73
+ ),
74
+ (
75
+ f"before{IMAGE_PLACEHOLDER}{IMAGE_PLACEHOLDER}after",
76
+ f"before<|vision_start|>{IMAGE_TOKEN * 2}<|vision_end|>\n"
77
+ f"<|vision_start|>{IMAGE_TOKEN * 3}<|vision_end|>\nafter",
78
+ [2, 3],
79
+ ),
80
+ ],
81
+ ids=["wrapped", "bare", "existing_newline", "adjacent_images"],
82
+)
83
+def test_image_expansion_matches_wrappers_and_newline_tokenization(
84
+ processor, prompt, expected_text, token_counts
85
+):
86
+ tokenizer = processor.info.get_tokenizer()
87
+ image_id = tokenizer.get_vocab()[IMAGE_TOKEN]
88
+ grids = torch.tensor([[1, 2, 2 * count] for count in token_counts])
89
+ mm_kwargs = MultiModalKwargsItems.from_hf_inputs(
90
+ {"image_grid_thw": grids},
91
+ {"image_grid_thw": MultiModalFieldConfig.batched("image")},
92
+ )
93
+ mm_items = MultiModalDataItems(
94
+ {"image": ImageProcessorItems([Image.new("RGB", (1, 1))] * len(token_counts))}
95
+ )
96
+ updates = processor._get_mm_prompt_updates(mm_items, {}, mm_kwargs)
97
+ prompt_ids = tokenizer.encode(prompt, add_special_tokens=False)
98
+ expected_ids = tokenizer.encode(expected_text, add_special_tokens=False)
99
+
100
+ actual_ids, placeholders = processor._apply_prompt_updates(prompt_ids, updates)
101
+
102
+ assert actual_ids == expected_ids
103
+ assert len(placeholders["image"]) == len(token_counts)
104
+ found = processor._find_mm_placeholders(actual_ids, updates)
105
+ search_offset = 0
106
+ for index, count in enumerate(token_counts):
107
+ placeholder = placeholders["image"][index]
108
+ expected_offset = expected_ids.index(image_id, search_offset)
109
+ assert placeholder.start_idx == expected_offset
110
+ assert placeholder.tokens == [image_id] * count
111
+ assert placeholder.to_range().get_num_embeds() == count
112
+ assert placeholder.to_range() == found["image"][index].to_range()
113
+ search_offset = expected_offset + count
114
+
115
+
116
+def test_text_only_prompt_preserves_original_token_ids(processor):
117
+ tokenizer = processor.info.get_tokenizer()
118
+ prompt_ids = tokenizer.encode("before\n", add_special_tokens=False)
119
+ prompt_ids += tokenizer.encode("\nafter", add_special_tokens=False)
120
+ # Deliberately use two newline tokens instead of their merged BPE token.
121
+ assert prompt_ids != tokenizer.encode("before\n\nafter", add_special_tokens=False)
122
+
123
+ actual_ids, placeholders = processor._apply_prompt_updates(prompt_ids, {})
124
+
125
+ assert actual_ids == prompt_ids
126
+ assert placeholders == {}
tests/models/multimodal/test_mapping.py
+30
@@ -184,6 +184,36 @@ def test_cosmos3_edge_checkpoint_weights_mapper():
184
)
185
186
187
+@pytest.mark.cpu_test
188
+def test_bailing_vl_mapper_handles_module_and_parameter_names():
189
+ from vllm.model_executor.models.bailing_moe_v3_vl import (
190
+ BailingMoeV3VLForConditionalGeneration,
191
+ )
192
+
193
+ mapper = BailingMoeV3VLForConditionalGeneration.hf_to_vllm_mapper
194
+ names = {
195
+ "model.visual": "visual",
196
+ "model.visual.blocks.0.attn.qkv.weight": "visual.blocks.0.attn.qkv.weight",
197
+ "model.linear_proj": "linear_proj",
198
+ "linear_proj.0": "linear_proj.linear_fc1",
199
+ "model.linear_proj.0.weight": "linear_proj.linear_fc1.weight",
200
+ "linear_proj.2": "linear_proj.linear_fc2",
201
+ "linear_proj.2.weight_scale": "linear_proj.linear_fc2.weight_scale",
202
+ "model.linear_proj.2.bias": "linear_proj.linear_fc2.bias",
203
+ "linear_proj.linear_fc1.weight": "linear_proj.linear_fc1.weight",
204
+ "model.linear_proj.linear_fc2.weight": "linear_proj.linear_fc2.weight",
205
+ "model.layers.0.attention.q_proj.weight": (
206
+ "language_model.model.layers.0.self_attn.q_proj.weight"
207
+ ),
208
+ "model.layers.0.mlp.experts.3.up_proj.weight": (
209
+ "language_model.model.layers.0.mlp.experts.3.up_proj.weight"
210
+ ),
211
+ "lm_head": "language_model.lm_head",
212
+ "lm_head.weight": "language_model.lm_head.weight",
213
+ }
214
+ assert mapper.apply_list(list(names)) == list(names.values())
215
+
216
+
217
def create_repo_dummy_weights(repo: str) -> Iterable[tuple[str, torch.Tensor]]:
218
"""Create weights from safetensors checkpoint metadata"""
219
metadata = try_get_safetensors_metadata(repo)
tests/models/registry.py
+12
@@ -796,6 +796,18 @@ _MULTIMODAL_EXAMPLE_MODELS = {
796
},
797
),
798
"BagelForConditionalGeneration": _HfExamplesInfo("ByteDance-Seed/BAGEL-7B-MoT"),
799
+ "BailingMoeV3VLForConditionalGeneration": _HfExamplesInfo(
800
+ "inclusionAI/Ling-3.0-flash-VL",
801
+ trust_remote_code=True,
802
+ is_available_online=True,
803
+ use_original_num_layers=True,
804
+ hf_overrides={
805
+ "text_config": {
806
+ "num_hidden_layers": 6,
807
+ "layer_types": ["linear_attention"] * 5 + ["full_attention"],
808
+ }
809
+ },
810
+ ),
811
"BeeForConditionalGeneration": _HfExamplesInfo(
812
"Open-Bee/Bee-8B-RL",
813
trust_remote_code=True,
tests/parser/engine/test_ling3.py
+7
-6
@@ -3,6 +3,8 @@
3
4
import json
5
6
+import pytest
7
+
8
from tests.parser.engine.replay_harness import MockTokenizer, _test_request
9
from vllm.reasoning import ReasoningParserManager
10
from vllm.tool_parsers import ToolParserManager
@@ -57,19 +59,18 @@ def test_ling3_disable_thinking_keeps_reasoning_as_content():
59
assert content == "<think>reason</think>answer"
60
61
60
-def test_ling3_enable_thinking_keeps_open_reasoning_as_content():
62
+@pytest.mark.parametrize("model_output", ["only reasoning", "<think>only reasoning"])
63
+def test_ling3_truncated_reasoning_stays_reasoning(model_output):
64
parser_cls = ReasoningParserManager.get_reasoning_parser("ling3")
65
parser = parser_cls(
66
_tokenizer(),
67
chat_template_kwargs={"enable_thinking": True},
68
)
69
67
- reasoning, content = parser.extract_reasoning(
68
- "<think>only reasoning", _test_request()
69
- )
70
+ reasoning, content = parser.extract_reasoning(model_output, _test_request())
71
71
- assert reasoning is None
72
- assert content == "only reasoning"
72
+ assert reasoning == "only reasoning"
73
+ assert content is None
74
75
76
def test_ling3_tool_call_without_newline():
tests/transformers_utils/test_bailing_moe_v3_vl_config.py
new
+159
@@ -0,0 +1,159 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import copy
5
+import json
6
+from pathlib import Path
7
+from typing import Any
8
+
9
+import pytest
10
+from transformers import AutoConfig
11
+
12
+from vllm.transformers_utils.config import get_config, uses_mrope
13
+from vllm.transformers_utils.configs.bailing_moe_v3_vl import (
14
+ BailingMoeV3TextConfig,
15
+ BailingMoeV3VisionConfig,
16
+ BailingMoeV3VLConfig,
17
+)
18
+
19
+
20
+def _bailing_v3_vl_config() -> dict[str, Any]:
21
+ return {
22
+ "architectures": ["BailingMoeV3VLForConditionalGeneration"],
23
+ "auto_map": {
24
+ "AutoConfig": ("configuration_bailing_moe_v3_vl.BailingMoeV3VLConfig")
25
+ },
26
+ "model_type": "bailing_moe_v3_vl",
27
+ "mrope_section": [8, 12, 12],
28
+ "text_config": {
29
+ "num_hidden_layers": 42,
30
+ "layer_group_size": 6,
31
+ "rope_theta": 6_000_000,
32
+ },
33
+ "vision_config": {"disable_merger_proj": True},
34
+ }
35
+
36
+
37
+def _write_config(path: Path, config: dict[str, Any]) -> None:
38
+ path.mkdir()
39
+ (path / "config.json").write_text(json.dumps(config), encoding="utf-8")
40
+
41
+
42
+def test_bailing_v3_vl_config_loads_without_remote_code(tmp_path: Path):
43
+ model_path = tmp_path / "model"
44
+ _write_config(model_path, _bailing_v3_vl_config())
45
+
46
+ config = get_config(model_path, trust_remote_code=False)
47
+
48
+ assert isinstance(config, BailingMoeV3VLConfig)
49
+ assert isinstance(config.text_config, BailingMoeV3TextConfig)
50
+ assert isinstance(config.vision_config, BailingMoeV3VisionConfig)
51
+ assert isinstance(
52
+ AutoConfig.from_pretrained(model_path, trust_remote_code=False),
53
+ BailingMoeV3VLConfig,
54
+ )
55
+ assert config.mrope_section == [8, 12, 12]
56
+ assert uses_mrope(config)
57
+
58
+ text_config = config.text_config
59
+ assert text_config.model_type == "bailing_hybrid"
60
+ assert text_config.architectures == ["BailingMoeV3ForCausalLM"]
61
+ assert text_config.rope_parameters["mrope_section"] == [8, 12, 12]
62
+ assert text_config.rope_parameters["rope_theta"] == 6_000_000
63
+ assert text_config.layer_types.count("linear_attention") == 35
64
+ assert [
65
+ layer_idx
66
+ for layer_idx, layer_type in enumerate(text_config.layer_types)
67
+ if layer_type == "full_attention"
68
+ ] == [5, 11, 17, 23, 29, 35, 41]
69
+ assert config.vision_config.disable_merger_proj is True
70
+
71
+ normalized = config.to_dict()
72
+ restored = BailingMoeV3VLConfig.from_dict(normalized)
73
+ assert isinstance(restored.text_config, BailingMoeV3TextConfig)
74
+ assert isinstance(restored.vision_config, BailingMoeV3VisionConfig)
75
+ assert restored.to_dict() == normalized
76
+
77
+
78
+def test_bailing_v3_vl_config_preserves_explicit_nested_values():
79
+ config_dict = _bailing_v3_vl_config()
80
+ config_dict["architectures"] = ["CustomBailingVLForConditionalGeneration"]
81
+ text_config = config_dict["text_config"]
82
+ text_config["architectures"] = ["CustomBailingForCausalLM"]
83
+ text_config["layer_types"] = (["linear_attention"] * 5 + ["full_attention"]) * 7
84
+ text_config["rope_theta"] = 1_000_000
85
+ text_config["rope_parameters"] = {
86
+ "rope_type": "default",
87
+ "rope_theta": 1_000_000,
88
+ "mrope_section": [8, 12, 12],
89
+ }
90
+ original = copy.deepcopy(config_dict)
91
+
92
+ config = BailingMoeV3VLConfig(**config_dict)
93
+
94
+ assert config_dict == original
95
+ assert config.architectures == ["CustomBailingVLForConditionalGeneration"]
96
+ assert config.text_config.architectures == ["CustomBailingForCausalLM"]
97
+ assert config.text_config.layer_types == text_config["layer_types"]
98
+ assert config.text_config.rope_parameters["rope_theta"] == 1_000_000
99
+ assert config.text_config.rope_parameters["mrope_section"] == [8, 12, 12]
100
+
101
+
102
+def test_bailing_v3_vl_config_normalizes_config_instance():
103
+ text_config = BailingMoeV3TextConfig()
104
+
105
+ config = BailingMoeV3VLConfig(
106
+ text_config=text_config,
107
+ mrope_section=[8, 12, 12],
108
+ )
109
+
110
+ assert config.text_config is text_config
111
+ assert text_config.rope_parameters["mrope_section"] == [8, 12, 12]
112
+
113
+
114
+def test_bailing_v3_text_config_trailing_and_legacy_layers():
115
+ expected = ["linear_attention", "linear_attention", "full_attention"] * 2 + [
116
+ "full_attention"
117
+ ] * 2
118
+ text_config = BailingMoeV3TextConfig(
119
+ num_hidden_layers=8,
120
+ layer_group_size=3,
121
+ )
122
+ assert text_config.layer_types == expected
123
+
124
+ text_config = BailingMoeV3TextConfig(
125
+ num_hidden_layers=8,
126
+ layer_group_size=3,
127
+ layers_block_type=["mamba", "mamba", "attention"] * 2 + ["attention"] * 2,
128
+ )
129
+ assert text_config.layer_types == expected
130
+
131
+
132
+def test_bailing_v3_text_config_accepts_legacy_rope_type():
133
+ text_config = BailingMoeV3TextConfig(rope_scaling={"type": "linear", "factor": 2.0})
134
+
135
+ assert text_config.rope_parameters["type"] == "linear"
136
+ assert text_config.rope_parameters["rope_type"] == "linear"
137
+
138
+
139
+def test_bailing_v3_vl_config_rejects_invalid_values():
140
+ with pytest.raises(ValueError, match="layer_group_size must be positive"):
141
+ BailingMoeV3TextConfig(layer_group_size=0)
142
+
143
+ with pytest.raises(ValueError, match="must match the layer_group_size schedule"):
144
+ BailingMoeV3TextConfig(
145
+ num_hidden_layers=8,
146
+ layer_group_size=3,
147
+ layer_types=["full_attention"] * 8,
148
+ )
149
+
150
+ with pytest.raises(ValueError, match="conflicting M-RoPE sections"):
151
+ BailingMoeV3VLConfig(
152
+ text_config={
153
+ "rope_parameters": {
154
+ "rope_type": "default",
155
+ "mrope_section": [4, 14, 14],
156
+ }
157
+ },
158
+ mrope_section=[8, 12, 12],
159
+ )
vllm/config/speculative.py
+12
-1
@@ -847,7 +847,18 @@ class SpeculativeConfig:
847
)
848
849
architectures = getattr(hf_config, "architectures", []) or []
850
- if initial_architecture == "BailingMoeV3ForCausalLM":
850
+ if initial_architecture == "BailingMoeV3VLForConditionalGeneration":
851
+ quantization_config = getattr(hf_config, "quantization_config", None)
852
+ hf_config = copy.deepcopy(hf_config.text_config)
853
+ if (
854
+ quantization_config is not None
855
+ and getattr(hf_config, "quantization_config", None) is None
856
+ ):
857
+ hf_config.quantization_config = copy.deepcopy(quantization_config)
858
+ if initial_architecture in (
859
+ "BailingMoeV3ForCausalLM",
860
+ "BailingMoeV3VLForConditionalGeneration",
861
+ ):
862
hf_config.model_type = "bailing_hybrid_v3_mtp"
863
elif (
864
hf_config.model_type == "bailing_hybrid"
vllm/model_executor/layers/rotary_embedding/__init__.py
+11
@@ -6,6 +6,7 @@ from typing import Any
6
7
import torch
8
9
+from .bailing_mrope import BailingMRotaryEmbedding
10
from .base import RotaryEmbedding
11
from .deepseek_scaling_rope import (
12
DeepseekScalingRotaryEmbedding,
@@ -97,6 +98,16 @@ def get_rope(
98
dtype,
99
**extra_kwargs,
100
)
101
+ elif scaling_type == "bailing_mrope":
102
+ rotary_emb = BailingMRotaryEmbedding(
103
+ head_size,
104
+ rotary_dim,
105
+ max_position,
106
+ base,
107
+ is_neox_style,
108
+ dtype,
109
+ mrope_section=rope_parameters["mrope_section"],
110
+ )
111
elif scaling_type == "default":
112
if "mrope_section" in rope_parameters:
113
rotary_emb = MRotaryEmbedding(
vllm/model_executor/layers/rotary_embedding/bailing_mrope.py
new
+351
@@ -0,0 +1,351 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import torch
5
+
6
+from vllm.triton_utils import tl, triton
7
+
8
+from .mrope import MRotaryEmbedding
9
+
10
+
11
+def _validate_bailing_mrope_config(
12
+ head_size: int,
13
+ rotary_dim: int,
14
+ mrope_section: list[int],
15
+) -> int:
16
+ if rotary_dim <= 0 or rotary_dim % 2 != 0 or rotary_dim > head_size:
17
+ raise ValueError(
18
+ "Bailing M-RoPE requires an even rotary_dim in (0, head_size], "
19
+ f"got rotary_dim={rotary_dim} and head_size={head_size}"
20
+ )
21
+ if len(mrope_section) != 3:
22
+ raise ValueError(
23
+ "Bailing M-RoPE expects [T, H, W] sections, "
24
+ f"got mrope_section={mrope_section}"
25
+ )
26
+
27
+ temporal_size, height_size, width_size = mrope_section
28
+ if any(section_size < 0 for section_size in mrope_section):
29
+ raise ValueError(
30
+ "Bailing M-RoPE sections must be non-negative, "
31
+ f"got mrope_section={mrope_section}"
32
+ )
33
+ if height_size != width_size:
34
+ raise ValueError(
35
+ "Bailing M-RoPE requires equal H and W sections, "
36
+ f"got height_size={height_size} and width_size={width_size}"
37
+ )
38
+ spatial_size = height_size + width_size
39
+ if spatial_size + temporal_size != rotary_dim // 2:
40
+ raise ValueError(
41
+ "Bailing M-RoPE sections must sum to rotary_dim // 2, got "
42
+ f"mrope_section={mrope_section} and rotary_dim={rotary_dim}"
43
+ )
44
+ return spatial_size
45
+
46
+
47
+def _validate_bailing_positions(
48
+ positions: torch.Tensor,
49
+ *,
50
+ require_2d: bool = False,
51
+) -> None:
52
+ if positions.dtype not in (torch.int32, torch.int64):
53
+ raise ValueError(
54
+ "Bailing M-RoPE positions must have int32 or int64 dtype, got "
55
+ f"dtype={positions.dtype}"
56
+ )
57
+ if positions.ndim == 1 and not require_2d:
58
+ return
59
+ if positions.ndim == 2:
60
+ if positions.shape[0] == 3:
61
+ return
62
+ raise ValueError(
63
+ "Bailing M-RoPE expects T/H/W positions with shape [3, N], "
64
+ f"got shape={tuple(positions.shape)}"
65
+ )
66
+ if require_2d:
67
+ raise ValueError(
68
+ "Bailing M-RoPE expects T/H/W positions with shape [3, N], "
69
+ f"got shape={tuple(positions.shape)}"
70
+ )
71
+ raise ValueError(
72
+ f"Bailing M-RoPE positions must be 1D or 2D, got shape={tuple(positions.shape)}"
73
+ )
74
+
75
+
76
+@triton.jit
77
+def _triton_bailing_mrope_forward(
78
+ q_ptr,
79
+ k_ptr,
80
+ positions_ptr,
81
+ cos_sin_cache_ptr,
82
+ q_stride_token,
83
+ q_stride_head,
84
+ q_stride_dim,
85
+ k_stride_token,
86
+ k_stride_head,
87
+ k_stride_dim,
88
+ positions_stride_axis,
89
+ positions_stride_token,
90
+ cache_stride_position,
91
+ cache_stride_dim,
92
+ n_qh: tl.constexpr,
93
+ n_kh: tl.constexpr,
94
+ rotary_dim: tl.constexpr,
95
+ pad_n_qh: tl.constexpr,
96
+ pad_n_kh: tl.constexpr,
97
+ pad_half_rotary_dim: tl.constexpr,
98
+ spatial_size: tl.constexpr,
99
+):
100
+ """Apply GPT-J-style H/W-interleaved, T-tail M-RoPE in place."""
101
+ token_idx = tl.program_id(0)
102
+ position_offset = token_idx * positions_stride_token
103
+ temporal_position = tl.load(positions_ptr + position_offset).to(tl.int64)
104
+ height_position = tl.load(
105
+ positions_ptr + positions_stride_axis + position_offset
106
+ ).to(tl.int64)
107
+ width_position = tl.load(
108
+ positions_ptr + 2 * positions_stride_axis + position_offset
109
+ ).to(tl.int64)
110
+
111
+ frequency_offsets = tl.arange(0, pad_half_rotary_dim)
112
+ frequency_mask = frequency_offsets < rotary_dim // 2
113
+ spatial_position = tl.where(
114
+ frequency_offsets % 2 == 0,
115
+ height_position,
116
+ width_position,
117
+ )
118
+ selected_position = tl.where(
119
+ frequency_offsets < spatial_size,
120
+ spatial_position,
121
+ temporal_position,
122
+ )
123
+ cache_offsets = (
124
+ selected_position * cache_stride_position + frequency_offsets * cache_stride_dim
125
+ )
126
+ cos_row = tl.load(
127
+ cos_sin_cache_ptr + cache_offsets,
128
+ mask=frequency_mask,
129
+ other=0.0,
130
+ ).to(tl.float32)
131
+ sin_row = tl.load(
132
+ cos_sin_cache_ptr + cache_offsets + (rotary_dim // 2) * cache_stride_dim,
133
+ mask=frequency_mask,
134
+ other=0.0,
135
+ ).to(tl.float32)
136
+
137
+ q_head_offsets = tl.arange(0, pad_n_qh)[:, None]
138
+ k_head_offsets = tl.arange(0, pad_n_kh)[:, None]
139
+ pair_offsets = frequency_offsets[None, :]
140
+ q_mask = (q_head_offsets < n_qh) & frequency_mask[None, :]
141
+ k_mask = (k_head_offsets < n_kh) & frequency_mask[None, :]
142
+
143
+ q_base = q_ptr + token_idx * q_stride_token
144
+ q_even_offsets = q_head_offsets * q_stride_head + 2 * pair_offsets * q_stride_dim
145
+ q_odd_offsets = q_even_offsets + q_stride_dim
146
+ q_even = tl.load(q_base + q_even_offsets, mask=q_mask, other=0.0).to(tl.float32)
147
+ q_odd = tl.load(q_base + q_odd_offsets, mask=q_mask, other=0.0).to(tl.float32)
148
+ q_even_cos = (q_even * cos_row[None, :]).to(q_ptr.type.element_ty).to(tl.float32)
149
+ q_odd_sin = (q_odd * sin_row[None, :]).to(q_ptr.type.element_ty).to(tl.float32)
150
+ q_odd_cos = (q_odd * cos_row[None, :]).to(q_ptr.type.element_ty).to(tl.float32)
151
+ q_even_sin = (q_even * sin_row[None, :]).to(q_ptr.type.element_ty).to(tl.float32)
152
+ tl.store(
153
+ q_base + q_even_offsets,
154
+ q_even_cos - q_odd_sin,
155
+ mask=q_mask,
156
+ )
157
+ tl.store(
158
+ q_base + q_odd_offsets,
159
+ q_odd_cos + q_even_sin,
160
+ mask=q_mask,
161
+ )
162
+
163
+ k_base = k_ptr + token_idx * k_stride_token
164
+ k_even_offsets = k_head_offsets * k_stride_head + 2 * pair_offsets * k_stride_dim
165
+ k_odd_offsets = k_even_offsets + k_stride_dim
166
+ k_even = tl.load(k_base + k_even_offsets, mask=k_mask, other=0.0).to(tl.float32)
167
+ k_odd = tl.load(k_base + k_odd_offsets, mask=k_mask, other=0.0).to(tl.float32)
168
+ k_even_cos = (k_even * cos_row[None, :]).to(k_ptr.type.element_ty).to(tl.float32)
169
+ k_odd_sin = (k_odd * sin_row[None, :]).to(k_ptr.type.element_ty).to(tl.float32)
170
+ k_odd_cos = (k_odd * cos_row[None, :]).to(k_ptr.type.element_ty).to(tl.float32)
171
+ k_even_sin = (k_even * sin_row[None, :]).to(k_ptr.type.element_ty).to(tl.float32)
172
+ tl.store(
173
+ k_base + k_even_offsets,
174
+ k_even_cos - k_odd_sin,
175
+ mask=k_mask,
176
+ )
177
+ tl.store(
178
+ k_base + k_odd_offsets,
179
+ k_odd_cos + k_even_sin,
180
+ mask=k_mask,
181
+ )
182
+
183
+
184
+def triton_bailing_mrope(
185
+ query: torch.Tensor,
186
+ key: torch.Tensor,
187
+ positions: torch.Tensor,
188
+ cos_sin_cache: torch.Tensor,
189
+ spatial_size: int,
190
+ head_size: int,
191
+ rotary_dim: int,
192
+) -> tuple[torch.Tensor, torch.Tensor]:
193
+ """Fused Bailing M-RoPE for packed or strided Q/K."""
194
+ _validate_bailing_positions(positions, require_2d=True)
195
+ num_tokens = positions.shape[-1]
196
+ if num_tokens == 0:
197
+ return query, key
198
+
199
+ query_3d = query.view(num_tokens, -1, head_size)
200
+ key_3d = key.view(num_tokens, -1, head_size)
201
+ n_q_heads = query_3d.shape[1]
202
+ n_k_heads = key_3d.shape[1]
203
+ pad_n_q_heads = triton.next_power_of_2(n_q_heads)
204
+ pad_n_k_heads = triton.next_power_of_2(n_k_heads)
205
+ pad_half_rotary_dim = triton.next_power_of_2(rotary_dim // 2)
206
+ _triton_bailing_mrope_forward[(num_tokens,)](
207
+ query_3d,
208
+ key_3d,
209
+ positions,
210
+ cos_sin_cache,
211
+ query_3d.stride(0),
212
+ query_3d.stride(1),
213
+ query_3d.stride(2),
214
+ key_3d.stride(0),
215
+ key_3d.stride(1),
216
+ key_3d.stride(2),
217
+ positions.stride(0),
218
+ positions.stride(1),
219
+ cos_sin_cache.stride(0),
220
+ cos_sin_cache.stride(1),
221
+ n_q_heads,
222
+ n_k_heads,
223
+ rotary_dim,
224
+ pad_n_q_heads,
225
+ pad_n_k_heads,
226
+ pad_half_rotary_dim,
227
+ spatial_size,
228
+ num_warps=4,
229
+ )
230
+ return query, key
231
+
232
+
233
+class BailingMRotaryEmbedding(MRotaryEmbedding):
234
+ """Bailing M-RoPE with alternating H/W frequencies and a temporal tail."""
235
+
236
+ def __init__(
237
+ self,
238
+ head_size: int,
239
+ rotary_dim: int,
240
+ max_position_embeddings: int,
241
+ base: float,
242
+ is_neox_style: bool,
243
+ dtype: torch.dtype,
244
+ mrope_section: list[int],
245
+ ) -> None:
246
+ self.spatial_size = _validate_bailing_mrope_config(
247
+ head_size, rotary_dim, mrope_section
248
+ )
249
+ super().__init__(
250
+ head_size,
251
+ rotary_dim,
252
+ max_position_embeddings,
253
+ base,
254
+ is_neox_style,
255
+ dtype,
256
+ mrope_section=mrope_section,
257
+ )
258
+
259
+ def select_cos_sin(
260
+ self,
261
+ positions: torch.Tensor,
262
+ cos_sin_cache: torch.Tensor | None = None,
263
+ ) -> tuple[torch.Tensor, torch.Tensor]:
264
+ _validate_bailing_positions(positions)
265
+ if cos_sin_cache is None:
266
+ cos_sin_cache = self.cos_sin_cache
267
+ cos_sin = cos_sin_cache[positions.to(torch.long)]
268
+ cos, sin = cos_sin.chunk(2, dim=-1)
269
+ if positions.ndim == 1:
270
+ return cos, sin
271
+
272
+ spatial_mask = torch.arange(self.spatial_size, device=cos.device) % 2 == 0
273
+ spatial_mask = spatial_mask.unsqueeze(0)
274
+ spatial_cos = torch.where(
275
+ spatial_mask,
276
+ cos[1, :, : self.spatial_size],
277
+ cos[2, :, : self.spatial_size],
278
+ )
279
+ spatial_sin = torch.where(
280
+ spatial_mask,
281
+ sin[1, :, : self.spatial_size],
282
+ sin[2, :, : self.spatial_size],
283
+ )
284
+ temporal_slice = slice(self.spatial_size, self.rotary_dim // 2)
285
+ return (
286
+ torch.cat((spatial_cos, cos[0, :, temporal_slice]), dim=-1),
287
+ torch.cat((spatial_sin, sin[0, :, temporal_slice]), dim=-1),
288
+ )
289
+
290
+ def forward_native(
291
+ self,
292
+ positions: torch.Tensor,
293
+ query: torch.Tensor,
294
+ key: torch.Tensor | None = None,
295
+ offsets: torch.Tensor | None = None,
296
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
297
+ if key is None:
298
+ raise ValueError("Bailing M-RoPE requires key states")
299
+
300
+ cos_sin_cache = self._match_cos_sin_cache_dtype(query)
301
+ cos, sin = self.select_cos_sin(positions, cos_sin_cache)
302
+ num_tokens = positions.shape[-1]
303
+ query_shape = query.shape
304
+ query = query.view(num_tokens, -1, self.head_size)
305
+ query_rot = self.apply_rotary_emb.forward_native(
306
+ query[..., : self.rotary_dim],
307
+ cos,
308
+ sin,
309
+ )
310
+ query = torch.cat((query_rot, query[..., self.rotary_dim :]), dim=-1)
311
+
312
+ key_shape = key.shape
313
+ key = key.view(num_tokens, -1, self.head_size)
314
+ key_rot = self.apply_rotary_emb.forward_native(
315
+ key[..., : self.rotary_dim],
316
+ cos,
317
+ sin,
318
+ )
319
+ key = torch.cat((key_rot, key[..., self.rotary_dim :]), dim=-1)
320
+ return query.reshape(query_shape), key.reshape(key_shape)
321
+
322
+ def forward_cuda(
323
+ self,
324
+ positions: torch.Tensor,
325
+ query: torch.Tensor,
326
+ key: torch.Tensor | None = None,
327
+ offsets: torch.Tensor | None = None,
328
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
329
+ if positions.ndim == 2 and not self.is_neox_style:
330
+ if key is None:
331
+ raise ValueError("Bailing M-RoPE requires key states")
332
+ cos_sin_cache = self._match_cos_sin_cache_dtype(query)
333
+ return triton_bailing_mrope(
334
+ query,
335
+ key,
336
+ positions,
337
+ cos_sin_cache,
338
+ self.spatial_size,
339
+ self.head_size,
340
+ self.rotary_dim,
341
+ )
342
+ return self.forward_native(positions, query, key, offsets)
343
+
344
+ def forward_cpu(
345
+ self,
346
+ positions: torch.Tensor,
347
+ query: torch.Tensor,
348
+ key: torch.Tensor | None = None,
349
+ offsets: torch.Tensor | None = None,
350
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
351
+ return self.forward_native(positions, query, key, offsets)
vllm/model_executor/models/bailing_moe_v3.py
+23
-4
@@ -171,6 +171,27 @@ def _build_rope_parameters(config: PretrainedConfig) -> dict | None:
171
return rope_parameters or None
172
173
174
+def _build_mla_rotary_embedding(
175
+ config: PretrainedConfig,
176
+ head_size: int,
177
+) -> nn.Module:
178
+ rope_parameters = _build_rope_parameters(config)
179
+ if rope_parameters is not None and "mrope_section" in rope_parameters:
180
+ rope_type = rope_parameters.get("rope_type", "default")
181
+ if rope_type != "default":
182
+ raise ValueError(
183
+ f"Bailing M-RoPE only supports rope_type='default', got {rope_type!r}"
184
+ )
185
+ rope_parameters["rope_type"] = "bailing_mrope"
186
+
187
+ return get_rope(
188
+ head_size=head_size,
189
+ max_position=getattr(config, "max_position_embeddings", 8192),
190
+ is_neox_style=False,
191
+ rope_parameters=rope_parameters,
192
+ )
193
+
194
+
195
def _get_layer_swiglu_limit(limit_list: list | None, layer_idx: int) -> float | None:
196
if limit_list is None or layer_idx >= len(limit_list):
197
return None
@@ -524,11 +545,9 @@ class BailingMoeV3MLAAttention(nn.Module):
545
prefix=f"{prefix}.dense",
546
)
547
527
- self.rotary_emb = get_rope(
548
+ self.rotary_emb = _build_mla_rotary_embedding(
549
+ config,
550
head_size=self.qk_rope_head_dim,
529
- max_position=getattr(config, "max_position_embeddings", 8192),
530
- is_neox_style=False,
531
- rope_parameters=_build_rope_parameters(config),
551
)
552
mla_modules = MLAModules(
553
kv_a_layernorm=self.kv_a_layernorm,
vllm/model_executor/models/bailing_moe_v3_mtp.py
+32
-5
@@ -33,9 +33,14 @@ from vllm.model_executor.models.bailing_moe_v3 import (
33
_maybe_pad_block_fp8_shared_expert_checkpoint_tensor,
34
_maybe_remap_ling_mxfp4_weight_names,
35
)
36
-from vllm.model_executor.models.interfaces import SupportsPP
36
+from vllm.model_executor.models.interfaces import (
37
+ MultiModalEmbeddings,
38
+ SupportsMultiModalEmbeddings,
39
+ SupportsPP,
40
+)
41
from vllm.model_executor.models.utils import (
42
PPMissingLayer,
43
+ _merge_multimodal_embeddings,
44
is_pp_missing_parameter,
45
maybe_prefix,
46
)
@@ -214,8 +219,16 @@ class BailingMoeV3MultiTokenPredictor(nn.Module):
219
return self.logits_processor(head, mtp_layer.shared_head(hidden_states))
220
221
217
-@support_torch_compile
218
-class BailingMoeV3MTPModel(nn.Module, SupportsPP):
222
+@support_torch_compile(
223
+ dynamic_arg_dims={
224
+ "input_ids": 0,
225
+ "positions": -1,
226
+ "hidden_states": 0,
227
+ "intermediate_tensors": 0,
228
+ "inputs_embeds": 0,
229
+ }
230
+)
231
+class BailingMoeV3MTPModel(nn.Module, SupportsPP, SupportsMultiModalEmbeddings):
232
hf_to_vllm_mapper = BailingMoeV3ForCausalLM.hf_to_vllm_mapper
233
234
packed_modules_mapping = {
@@ -242,8 +255,22 @@ class BailingMoeV3MTPModel(nn.Module, SupportsPP):
255
def share_lm_head(self, lm_head: nn.Module) -> None:
256
self.lm_head = lm_head
257
245
- def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
246
- return self.model.embed_input_ids(input_ids)
258
+ def embed_input_ids(
259
+ self,
260
+ input_ids: torch.Tensor,
261
+ multimodal_embeddings: MultiModalEmbeddings | None = None,
262
+ *,
263
+ is_multimodal: torch.Tensor | None = None,
264
+ ) -> torch.Tensor:
265
+ inputs_embeds = self.model.embed_input_ids(input_ids)
266
+ if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
267
+ return inputs_embeds
268
+ assert is_multimodal is not None
269
+ return _merge_multimodal_embeddings(
270
+ inputs_embeds=inputs_embeds,
271
+ multimodal_embeddings=multimodal_embeddings,
272
+ is_multimodal=is_multimodal,
273
+ )
274
275
def forward(
276
self,
vllm/model_executor/models/bailing_moe_v3_vl.py
new
+527
@@ -0,0 +1,527 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Inference-only Bailing MoE V3 vision-language model."""
4
+
5
+import copy
6
+from collections.abc import Iterable, Mapping, Sequence
7
+from typing import Any
8
+
9
+import numpy as np
10
+import regex as re
11
+import torch
12
+import torch.nn as nn
13
+from transformers import AutoProcessor, BatchFeature
14
+
15
+from vllm.config import VllmConfig
16
+from vllm.config.multimodal import BaseDummyOptions
17
+from vllm.inputs import MultiModalDataDict
18
+from vllm.model_executor.layers.linear import (
19
+ ColumnParallelLinear,
20
+ RowParallelLinear,
21
+)
22
+from vllm.model_executor.layers.quantization import QuantizationConfig
23
+from vllm.model_executor.layers.quantization.fp8 import Fp8Config
24
+from vllm.model_executor.models.module_mapping import MultiModelKeys
25
+from vllm.multimodal import MULTIMODAL_REGISTRY
26
+from vllm.multimodal.inputs import (
27
+ MultiModalFeatureSpec,
28
+ MultiModalFieldConfig,
29
+ MultiModalKwargsItems,
30
+)
31
+from vllm.multimodal.parse import MultiModalDataItems
32
+from vllm.multimodal.processing import (
33
+ BaseDummyInputsBuilder,
34
+ BaseMultiModalProcessor,
35
+ PromptReplacement,
36
+ PromptUpdate,
37
+)
38
+from vllm.multimodal.processing.processor import (
39
+ MultiModalPromptUpdates,
40
+ PlaceholderFeaturesInfo,
41
+)
42
+from vllm.sequence import IntermediateTensors
43
+
44
+from .bailing_moe_v3 import BailingMoeV3ForCausalLM
45
+from .interfaces import (
46
+ HasInnerState,
47
+ IsHybrid,
48
+ MultiModalEmbeddings,
49
+ SupportsMRoPE,
50
+ SupportsMultiModal,
51
+ SupportsPP,
52
+ _require_is_multimodal,
53
+)
54
+from .qwen2_5_vl import (
55
+ Qwen2_5_VLImageEmbeddingInputs,
56
+ Qwen2_5_VLImageInputs,
57
+ Qwen2_5_VLImagePixelInputs,
58
+)
59
+from .qwen2_vl import (
60
+ Qwen2VLProcessingInfo,
61
+ _create_qwen2vl_field_factory,
62
+)
63
+from .qwen3_vl import Qwen3_VisionTransformer
64
+from .utils import (
65
+ AutoWeightsLoader,
66
+ WeightsMapper,
67
+ _merge_multimodal_embeddings,
68
+ maybe_prefix,
69
+)
70
+
71
+IMAGE_TOKEN = "<|image_pad|>"
72
+IMAGE_PLACEHOLDER = f"<|vision_start|>{IMAGE_TOKEN}<|vision_end|>"
73
+
74
+
75
+class BailingMoeV3VLProcessingInfo(Qwen2VLProcessingInfo):
76
+ """Processing metadata for the remote Bailing multimodal processor."""
77
+
78
+ def get_hf_config(self):
79
+ return self.ctx.get_hf_config()
80
+
81
+ def get_hf_processor(self, **kwargs: object) -> AutoProcessor:
82
+ # The checkpoint supplies its processor through trust_remote_code.
83
+ return self.ctx.get_hf_processor(**kwargs)
84
+
85
+ def get_supported_mm_limits(self) -> Mapping[str, int | None]:
86
+ return {"image": None}
87
+
88
+ def get_mm_max_tokens_per_item(
89
+ self,
90
+ seq_len: int,
91
+ mm_counts: Mapping[str, int],
92
+ ) -> Mapping[str, int]:
93
+ return {"image": self.get_max_image_tokens()}
94
+
95
+
96
+class BailingMoeV3VLDummyInputsBuilder(
97
+ BaseDummyInputsBuilder[BailingMoeV3VLProcessingInfo]
98
+):
99
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
100
+ return IMAGE_PLACEHOLDER * mm_counts.get("image", 0)
101
+
102
+ def get_dummy_mm_data(
103
+ self,
104
+ seq_len: int,
105
+ mm_counts: Mapping[str, int],
106
+ mm_options: Mapping[str, BaseDummyOptions],
107
+ ) -> MultiModalDataDict:
108
+ target_width, target_height = self.info.get_image_size_with_most_features()
109
+ return {
110
+ "image": self._get_dummy_images(
111
+ width=target_width,
112
+ height=target_height,
113
+ num_images=mm_counts.get("image", 0),
114
+ overrides=mm_options.get("image"),
115
+ )
116
+ }
117
+
118
+
119
+class BailingMoeV3VLMultiModalProcessor(
120
+ BaseMultiModalProcessor[BailingMoeV3VLProcessingInfo]
121
+):
122
+ def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
123
+ return self.dummy_inputs.get_dummy_text(mm_counts)
124
+
125
+ def _apply_prompt_updates(
126
+ self,
127
+ token_ids: list[int],
128
+ mm_prompt_updates: MultiModalPromptUpdates,
129
+ ) -> tuple[list[int], Mapping[str, list[PlaceholderFeaturesInfo]]]:
130
+ if image_updates := mm_prompt_updates.get("image"):
131
+ tokenizer = self.info.get_tokenizer()
132
+ # HF adds a newline after each image before tokenization. Re-encode
133
+ # the text so it can merge with existing whitespace at the boundary.
134
+ text = re.sub(
135
+ f"{re.escape(IMAGE_PLACEHOLDER)}|{re.escape(IMAGE_TOKEN)}",
136
+ lambda _: IMAGE_PLACEHOLDER + "\n",
137
+ tokenizer.decode(token_ids),
138
+ count=len(image_updates),
139
+ )
140
+ token_ids = tokenizer.encode(text, add_special_tokens=False)
141
+
142
+ return super()._apply_prompt_updates(token_ids, mm_prompt_updates)
143
+
144
+ def _get_mm_fields_config(
145
+ self,
146
+ hf_inputs: BatchFeature,
147
+ hf_processor_mm_kwargs: Mapping[str, object],
148
+ ) -> Mapping[str, MultiModalFieldConfig]:
149
+ merge_size = self.info.get_hf_config().vision_config.spatial_merge_size
150
+ fields = _create_qwen2vl_field_factory(merge_size)(hf_inputs)
151
+ return {
152
+ key: fields[key]
153
+ for key in ("pixel_values", "image_embeds", "image_grid_thw")
154
+ }
155
+
156
+ def _get_prompt_updates(
157
+ self,
158
+ mm_items: MultiModalDataItems,
159
+ hf_processor_mm_kwargs: Mapping[str, Any],
160
+ out_mm_kwargs: MultiModalKwargsItems,
161
+ ) -> Sequence[PromptUpdate]:
162
+ tokenizer = self.info.get_tokenizer()
163
+ image_token_id = tokenizer.get_vocab()[IMAGE_TOKEN]
164
+ merge_size = self.info.get_hf_config().vision_config.spatial_merge_size
165
+ merge_unit = merge_size**2
166
+
167
+ def get_image_replacement(item_idx: int) -> list[int]:
168
+ out_item = out_mm_kwargs["image"][item_idx]
169
+ image_grid_thw = out_item["image_grid_thw"].data
170
+ assert isinstance(image_grid_thw, torch.Tensor)
171
+ num_image_tokens = int(image_grid_thw.prod()) // merge_unit
172
+ return [image_token_id] * num_image_tokens
173
+
174
+ return [
175
+ PromptReplacement(
176
+ modality="image",
177
+ target=[image_token_id],
178
+ replacement=get_image_replacement,
179
+ )
180
+ ]
181
+
182
+
183
+class BailingMoeV3VisionPatchMerger(nn.Module):
184
+ """Normalize patches and merge each 2x2 group without an MLP."""
185
+
186
+ def __init__(
187
+ self,
188
+ context_dim: int,
189
+ spatial_merge_size: int,
190
+ norm_eps: float = 1e-6,
191
+ ) -> None:
192
+ super().__init__()
193
+ self.hidden_size = context_dim * spatial_merge_size**2
194
+ self.norm = nn.LayerNorm(context_dim, eps=norm_eps)
195
+
196
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
197
+ hidden_states = self.norm(hidden_states)
198
+ return hidden_states.reshape(-1, self.hidden_size)
199
+
200
+
201
+class BailingMoeV3VisionTransformer(Qwen3_VisionTransformer):
202
+ """Qwen3 ViT with Bailing's parameter-free patch merger."""
203
+
204
+ def __init__(
205
+ self,
206
+ vision_config: Any,
207
+ norm_eps: float = 1e-6,
208
+ quant_config: QuantizationConfig | None = None,
209
+ prefix: str = "",
210
+ ) -> None:
211
+ # Honor whole-tower exclusions before the parent creates linear layers.
212
+ if (
213
+ isinstance(quant_config, Fp8Config)
214
+ and prefix in quant_config.ignored_layers
215
+ ):
216
+ quant_config = None
217
+
218
+ vision_config = copy.deepcopy(vision_config)
219
+ vision_config.deepstack_visual_indexes = []
220
+ merged_hidden_size = (
221
+ vision_config.hidden_size * vision_config.spatial_merge_size**2
222
+ )
223
+ vision_config.out_hidden_size = merged_hidden_size
224
+
225
+ super().__init__(
226
+ vision_config,
227
+ norm_eps=norm_eps,
228
+ quant_config=quant_config,
229
+ prefix=prefix,
230
+ )
231
+ self.merger = BailingMoeV3VisionPatchMerger(
232
+ context_dim=vision_config.hidden_size,
233
+ spatial_merge_size=vision_config.spatial_merge_size,
234
+ norm_eps=norm_eps,
235
+ )
236
+ self.deepstack_visual_indexes = []
237
+ self.deepstack_merger_list = nn.ModuleList()
238
+ self.out_hidden_size = merged_hidden_size
239
+
240
+
241
+class BailingMoeV3VLProjector(nn.Module):
242
+ """Tensor-parallel MLP mapping merged vision features to text width."""
243
+
244
+ def __init__(
245
+ self,
246
+ vision_hidden_size: int,
247
+ text_hidden_size: int,
248
+ quant_config: QuantizationConfig | None = None,
249
+ prefix: str = "",
250
+ ) -> None:
251
+ super().__init__()
252
+ if (
253
+ isinstance(quant_config, Fp8Config)
254
+ and prefix in quant_config.ignored_layers
255
+ ):
256
+ quant_config = None
257
+
258
+ self.linear_fc1 = ColumnParallelLinear(
259
+ vision_hidden_size,
260
+ text_hidden_size,
261
+ bias=True,
262
+ quant_config=quant_config,
263
+ prefix=maybe_prefix(prefix, "linear_fc1"),
264
+ )
265
+ self.act_fn = nn.GELU()
266
+ self.linear_fc2 = RowParallelLinear(
267
+ text_hidden_size,
268
+ text_hidden_size,
269
+ bias=True,
270
+ quant_config=quant_config,
271
+ prefix=maybe_prefix(prefix, "linear_fc2"),
272
+ )
273
+
274
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
275
+ hidden_states, _ = self.linear_fc1(hidden_states)
276
+ hidden_states = self.act_fn(hidden_states)
277
+ hidden_states, _ = self.linear_fc2(hidden_states)
278
+ return hidden_states
279
+
280
+
281
+@MULTIMODAL_REGISTRY.register_processor(
282
+ BailingMoeV3VLMultiModalProcessor,
283
+ info=BailingMoeV3VLProcessingInfo,
284
+ dummy_inputs=BailingMoeV3VLDummyInputsBuilder,
285
+)
286
+class BailingMoeV3VLForConditionalGeneration(
287
+ nn.Module,
288
+ HasInnerState,
289
+ IsHybrid,
290
+ SupportsMultiModal,
291
+ SupportsPP,
292
+ SupportsMRoPE,
293
+):
294
+ """Native vLLM wrapper for ``BailingMoeV3VLConfig`` checkpoints."""
295
+
296
+ packed_modules_mapping = {
297
+ **BailingMoeV3ForCausalLM.packed_modules_mapping,
298
+ # The vision checkpoint already stores Q/K/V in one tensor.
299
+ "qkv": ["qkv"],
300
+ }
301
+
302
+ hf_to_vllm_mapper = BailingMoeV3ForCausalLM.hf_to_vllm_mapper | WeightsMapper(
303
+ orig_to_new_prefix={
304
+ # Match module names as well as parameter names for quantization.
305
+ "model.visual": "visual",
306
+ "model.linear_proj": "linear_proj",
307
+ "linear_proj.0": "linear_proj.linear_fc1",
308
+ "linear_proj.2": "linear_proj.linear_fc2",
309
+ "lm_head": "language_model.lm_head",
310
+ "model.": "language_model.model.",
311
+ }
312
+ )
313
+
314
+ @classmethod
315
+ def get_placeholder_str(cls, modality: str, i: int) -> str | None:
316
+ if modality.startswith("image"):
317
+ return IMAGE_PLACEHOLDER
318
+ raise ValueError("Only image modality is supported")
319
+
320
+ @classmethod
321
+ def get_mamba_state_shape_from_config(
322
+ cls, vllm_config: VllmConfig
323
+ ) -> tuple[tuple[int, int], tuple[int, int, int]]:
324
+ text_config = vllm_config.model_config.hf_config.text_config
325
+ return BailingMoeV3ForCausalLM.get_mamba_state_shape_from_config(
326
+ vllm_config.with_hf_config(text_config)
327
+ )
328
+
329
+ @classmethod
330
+ def get_mamba_state_dtype_from_config(
331
+ cls, vllm_config: VllmConfig
332
+ ) -> tuple[torch.dtype, torch.dtype]:
333
+ text_config = vllm_config.model_config.hf_config.text_config
334
+ return BailingMoeV3ForCausalLM.get_mamba_state_dtype_from_config(
335
+ vllm_config.with_hf_config(text_config)
336
+ )
337
+
338
+ @classmethod
339
+ def get_mamba_state_copy_func(cls) -> tuple:
340
+ return BailingMoeV3ForCausalLM.get_mamba_state_copy_func()
341
+
342
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "model") -> None:
343
+ super().__init__()
344
+ config = vllm_config.model_config.hf_config
345
+ vision_config = config.vision_config
346
+ text_config = config.text_config
347
+ quant_config = vllm_config.quant_config
348
+
349
+ self.config = config
350
+ self.model_config = vllm_config.model_config
351
+
352
+ merged_vision_size = vision_config.hidden_size * (
353
+ vision_config.spatial_merge_size**2
354
+ )
355
+ with self._mark_tower_model(vllm_config, "image"):
356
+ self.visual = BailingMoeV3VisionTransformer(
357
+ vision_config,
358
+ norm_eps=getattr(config, "rms_norm_eps", 1e-6),
359
+ quant_config=quant_config,
360
+ prefix=maybe_prefix(prefix, "visual"),
361
+ )
362
+ self.linear_proj = BailingMoeV3VLProjector(
363
+ vision_hidden_size=merged_vision_size,
364
+ text_hidden_size=text_config.hidden_size,
365
+ quant_config=quant_config,
366
+ prefix=maybe_prefix(prefix, "linear_proj"),
367
+ )
368
+
369
+ with self._mark_language_model(vllm_config):
370
+ self.language_model = BailingMoeV3ForCausalLM(
371
+ vllm_config=vllm_config.with_hf_config(text_config),
372
+ prefix=maybe_prefix(prefix, "language_model"),
373
+ )
374
+
375
+ self.make_empty_intermediate_tensors = (
376
+ self.language_model.make_empty_intermediate_tensors
377
+ )
378
+
379
+ def _parse_and_validate_image_input(
380
+ self, **kwargs: object
381
+ ) -> Qwen2_5_VLImageInputs | None:
382
+ pixel_values = kwargs.pop("pixel_values", None)
383
+ image_embeds = kwargs.pop("image_embeds", None)
384
+ image_grid_thw = kwargs.pop("image_grid_thw", None)
385
+
386
+ if pixel_values is None and image_embeds is None:
387
+ return None
388
+ if pixel_values is not None:
389
+ return Qwen2_5_VLImagePixelInputs(
390
+ type="pixel_values",
391
+ pixel_values=pixel_values,
392
+ image_grid_thw=image_grid_thw,
393
+ )
394
+ return Qwen2_5_VLImageEmbeddingInputs(
395
+ type="image_embeds",
396
+ image_embeds=image_embeds,
397
+ image_grid_thw=image_grid_thw,
398
+ )
399
+
400
+ def _process_image_input(
401
+ self, image_input: Qwen2_5_VLImageInputs
402
+ ) -> tuple[torch.Tensor, ...]:
403
+ image_grid_thw = image_input["image_grid_thw"]
404
+ assert image_grid_thw.ndim == 2
405
+
406
+ if image_input["type"] == "image_embeds":
407
+ image_embeds = image_input["image_embeds"]
408
+ else:
409
+ pixel_values = image_input["pixel_values"].type(self.visual.dtype)
410
+ vision_features = self.visual(
411
+ pixel_values,
412
+ grid_thw=image_grid_thw,
413
+ )
414
+ image_embeds = self.linear_proj(vision_features)
415
+
416
+ image_embeds = image_embeds.to(dtype=self.visual.dtype)
417
+ merge_unit = self.visual.spatial_merge_size**2
418
+ sizes = (image_grid_thw.prod(-1) // merge_unit).tolist()
419
+ return image_embeds.split(sizes)
420
+
421
+ def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
422
+ image_input = self._parse_and_validate_image_input(**kwargs)
423
+ if image_input is None:
424
+ return []
425
+ return self._process_image_input(image_input)
426
+
427
+ def embed_input_ids(
428
+ self,
429
+ input_ids: torch.Tensor,
430
+ multimodal_embeddings: MultiModalEmbeddings | None = None,
431
+ *,
432
+ is_multimodal: torch.Tensor | None = None,
433
+ ) -> torch.Tensor:
434
+ inputs_embeds = self._embed_text_input_ids(
435
+ input_ids,
436
+ self.language_model.embed_input_ids,
437
+ is_multimodal=is_multimodal,
438
+ )
439
+ if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
440
+ return inputs_embeds
441
+
442
+ return _merge_multimodal_embeddings(
443
+ inputs_embeds=inputs_embeds,
444
+ multimodal_embeddings=multimodal_embeddings,
445
+ is_multimodal=_require_is_multimodal(is_multimodal),
446
+ )
447
+
448
+ def get_mrope_input_positions(
449
+ self,
450
+ input_tokens: list[int],
451
+ mm_features: list[MultiModalFeatureSpec],
452
+ ) -> tuple[torch.Tensor, int]:
453
+ position_blocks: list[np.ndarray] = []
454
+ consumed = 0
455
+ merge_size = self.config.vision_config.spatial_merge_size
456
+
457
+ for mm_feature in sorted(mm_features, key=lambda f: f.mm_position.offset):
458
+ if mm_feature.modality != "image":
459
+ raise ValueError(f"Unsupported modality: {mm_feature.modality}")
460
+
461
+ offset = mm_feature.mm_position.offset
462
+ text_len = offset - consumed
463
+ start = position_blocks[-1].max() + 1 if position_blocks else 0
464
+ position_blocks.append(
465
+ np.broadcast_to(np.arange(text_len), (3, text_len)) + start
466
+ )
467
+
468
+ assert mm_feature.data is not None
469
+ image_grid_thw = mm_feature.data["image_grid_thw"].data
470
+ assert isinstance(image_grid_thw, torch.Tensor)
471
+ t, h, w = image_grid_thw.tolist()
472
+ assert t == 1, f"Image must have one temporal grid, got {t}"
473
+ grid = np.indices((t, h // merge_size, w // merge_size))
474
+ position_blocks.append(grid.reshape(3, -1) + text_len + start)
475
+ consumed = offset + t * (h // merge_size) * (w // merge_size)
476
+
477
+ if consumed < len(input_tokens):
478
+ start = position_blocks[-1].max() + 1 if position_blocks else 0
479
+ text_len = len(input_tokens) - consumed
480
+ position_blocks.append(
481
+ np.broadcast_to(np.arange(text_len), (3, text_len)) + start
482
+ )
483
+
484
+ if not position_blocks:
485
+ positions = np.empty((3, 0), dtype=np.int64)
486
+ return torch.from_numpy(positions), 0
487
+
488
+ positions = np.concatenate(position_blocks, axis=1).reshape(3, -1)
489
+ position_delta = int(positions.max() + 1 - len(input_tokens))
490
+ return torch.from_numpy(positions), position_delta
491
+
492
+ def forward(
493
+ self,
494
+ input_ids: torch.Tensor | None,
495
+ positions: torch.Tensor,
496
+ intermediate_tensors: IntermediateTensors | None = None,
497
+ inputs_embeds: torch.Tensor | None = None,
498
+ **kwargs: object,
499
+ ) -> torch.Tensor | IntermediateTensors:
500
+ if intermediate_tensors is not None:
501
+ inputs_embeds = None
502
+ return self.language_model(
503
+ input_ids=input_ids,
504
+ positions=positions,
505
+ intermediate_tensors=intermediate_tensors,
506
+ inputs_embeds=inputs_embeds,
507
+ )
508
+
509
+ def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
510
+ return self.language_model.compute_logits(hidden_states)
511
+
512
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
513
+ loader = AutoWeightsLoader(self)
514
+ return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
515
+
516
+ def get_mm_mapping(self) -> MultiModelKeys:
517
+ return MultiModelKeys.from_string_field(
518
+ language_model="language_model",
519
+ connector="linear_proj",
520
+ tower_model="visual.",
521
+ )
522
+
523
+ def get_num_mm_encoder_tokens(self, num_image_tokens: int) -> int:
524
+ return num_image_tokens * self.visual.spatial_merge_size**2
525
+
526
+ def get_num_mm_connector_tokens(self, num_vision_tokens: int) -> int:
527
+ return num_vision_tokens // self.visual.spatial_merge_size**2
vllm/model_executor/models/registry.py
+4
@@ -350,6 +350,10 @@ _MULTIMODAL_MODELS = {
350
"AudioFlamingo3ForConditionalGeneration",
351
),
352
"BagelForConditionalGeneration": ("bagel", "BagelForConditionalGeneration"),
353
+ "BailingMoeV3VLForConditionalGeneration": (
354
+ "bailing_moe_v3_vl",
355
+ "BailingMoeV3VLForConditionalGeneration",
356
+ ),
357
"BeeForConditionalGeneration": ("bee", "BeeForConditionalGeneration"),
358
"Blip2ForConditionalGeneration": ("blip2", "Blip2ForConditionalGeneration"),
359
"Cohere2VisionForConditionalGeneration": (
vllm/parser/ling3.py
+1
-4
@@ -71,10 +71,7 @@ class Ling3Parser(Glm47MoeParser):
71
if not self.thinking_enabled:
72
return None, model_output
73
74
- reasoning, content = super().extract_reasoning(model_output, request)
75
- if reasoning and not content and "<tool_call>" not in model_output:
76
- return None, reasoning
77
- return reasoning, content
74
+ return super().extract_reasoning(model_output, request)
75
76
77
(
vllm/transformers_utils/config.py
+1
@@ -88,6 +88,7 @@ _CONFIG_REGISTRY: dict[str, type[PretrainedConfig]] = LazyConfigDict(
88
afmoe="AfmoeConfig",
89
axk1="AXK1Config",
90
bagel="BagelConfig",
91
+ bailing_moe_v3_vl="BailingMoeV3VLConfig",
92
chatglm="ChatGLMConfig",
93
modernvbert="ColModernVBertConfig",
94
colpali="ColPaliConfig",
vllm/transformers_utils/configs/__init__.py
+6
@@ -18,6 +18,9 @@ _CLASS_TO_MODULE: dict[str, str] = {
18
"AfmoeConfig": "vllm.transformers_utils.configs.afmoe",
19
"AXK1Config": "vllm.transformers_utils.configs.AXK1",
20
"BagelConfig": "vllm.transformers_utils.configs.bagel",
21
+ "BailingMoeV3TextConfig": "vllm.transformers_utils.configs.bailing_moe_v3_vl",
22
+ "BailingMoeV3VisionConfig": "vllm.transformers_utils.configs.bailing_moe_v3_vl",
23
+ "BailingMoeV3VLConfig": "vllm.transformers_utils.configs.bailing_moe_v3_vl",
24
"ChatGLMConfig": "vllm.transformers_utils.configs.chatglm",
25
"ColModernVBertConfig": "vllm.transformers_utils.configs.colmodernvbert",
26
"ColPaliConfig": "vllm.transformers_utils.configs.colpali",
@@ -114,6 +117,9 @@ __all__ = [
117
"AfmoeConfig",
118
"AXK1Config",
119
"BagelConfig",
120
+ "BailingMoeV3TextConfig",
121
+ "BailingMoeV3VisionConfig",
122
+ "BailingMoeV3VLConfig",
123
"ChatGLMConfig",
124
"ColModernVBertConfig",
125
"ColPaliConfig",
vllm/transformers_utils/configs/bailing_moe_v3_vl.py
new
+345
@@ -0,0 +1,345 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+# Copyright 2025 Antgroup and The HuggingFace Inc. team. All rights reserved.
4
+# Adapted from
5
+# https://huggingface.co/inclusionAI/Ling-3.0-flash/blob/51fd444268df5267074ef0b289de0e1c4cb9b381/configuration_bailing_moe_v3.py
6
+"""Bailing V3 vision-language model configuration."""
7
+
8
+from __future__ import annotations
9
+
10
+from typing import Any
11
+
12
+from transformers.configuration_utils import PretrainedConfig
13
+
14
+
15
+def _normalize_mrope_parameters(
16
+ config: PretrainedConfig,
17
+ mrope_section: list[int],
18
+) -> None:
19
+ """Add the top-level Bailing M-RoPE split to a text config."""
20
+ rope_parameters = dict(getattr(config, "rope_parameters", None) or {})
21
+ text_mrope_section = rope_parameters.get("mrope_section")
22
+ if text_mrope_section is not None and list(text_mrope_section) != mrope_section:
23
+ raise ValueError(
24
+ "Bailing V3 VL has conflicting M-RoPE sections: "
25
+ f"text={text_mrope_section}, top-level={mrope_section}"
26
+ )
27
+
28
+ if "rope_type" not in rope_parameters and "type" not in rope_parameters:
29
+ rope_parameters["rope_type"] = "default"
30
+ rope_parameters.setdefault("rope_theta", getattr(config, "rope_theta", 10_000))
31
+ rope_parameters["mrope_section"] = list(mrope_section)
32
+ config.rope_parameters = rope_parameters
33
+
34
+
35
+def _build_layer_types(
36
+ num_hidden_layers: int,
37
+ layer_group_size: int,
38
+) -> list[str]:
39
+ if layer_group_size <= 0:
40
+ raise ValueError(
41
+ f"Bailing V3 VL layer_group_size must be positive, got {layer_group_size}"
42
+ )
43
+
44
+ grouped_layers = num_hidden_layers // layer_group_size * layer_group_size
45
+ return [
46
+ "linear_attention"
47
+ if ((layer_idx + 1) % layer_group_size != 0 and layer_idx < grouped_layers)
48
+ else "full_attention"
49
+ for layer_idx in range(num_hidden_layers)
50
+ ]
51
+
52
+
53
+class BailingMoeV3TextConfig(PretrainedConfig):
54
+ model_type = "bailing_hybrid"
55
+ base_config_key = "text_config"
56
+ keys_to_ignore_at_inference = ["past_key_values"]
57
+ ignore_keys_at_rope_validation = {"mrope_section"}
58
+
59
+ def __init__(
60
+ self,
61
+ *,
62
+ vocab_size: int = 157184,
63
+ hidden_size: int = 2048,
64
+ intermediate_size: int = 5120,
65
+ num_hidden_layers: int = 20,
66
+ num_attention_heads: int = 16,
67
+ num_key_value_heads: int = 4,
68
+ hidden_act: str = "silu",
69
+ use_qkv_bias: bool = False,
70
+ use_bias: bool = False,
71
+ rms_norm_eps: float = 1e-6,
72
+ tie_word_embeddings: bool = False,
73
+ embedding_dropout: float = 0.0,
74
+ attention_dropout: float = 0.0,
75
+ output_dropout: float = 0.0,
76
+ initializer_range: float = 0.02,
77
+ max_position_embeddings: int = 32768,
78
+ rope_theta: float = 600000.0,
79
+ rope_parameters: dict[str, Any] | None = None,
80
+ rope_scaling: dict[str, Any] | None = None,
81
+ partial_rotary_factor: float | None = None,
82
+ mrope_section: list[int] | tuple[int, ...] | None = None,
83
+ use_cache: bool = True,
84
+ max_window_layers: int = 20,
85
+ pad_token_id: int = 156892,
86
+ eos_token_id: int = 156892,
87
+ num_experts: int = 256,
88
+ num_shared_experts: int = 1,
89
+ num_experts_per_tok: int = 8,
90
+ n_group: int = 8,
91
+ topk_group: int = 4,
92
+ moe_intermediate_size: int = 512,
93
+ moe_shared_expert_intermediate_size: int = 512,
94
+ first_k_dense_replace: int = 1,
95
+ head_dim: int | None = 128,
96
+ output_router_logits: bool = False,
97
+ use_qk_norm: bool = True,
98
+ num_nextn_predict_layers: int = 0,
99
+ mtp_loss_scaling_factor: float = 0,
100
+ moe_router_enable_expert_bias: bool = True,
101
+ routed_scaling_factor: float = 1.0,
102
+ layer_group_size: int = 5,
103
+ layer_types: list[str] | None = None,
104
+ layers_block_type: list[str] | None = None,
105
+ kv_lora_rank: int = 512,
106
+ q_lora_rank: int | None = None,
107
+ qk_rope_head_dim: int = 64,
108
+ v_head_dim: int = 128,
109
+ qk_nope_head_dim: int = 128,
110
+ rope_interleave: bool = True,
111
+ score_function: str = "sigmoid",
112
+ scoring_func: str = "sigmoid",
113
+ seq_aux: bool = True,
114
+ topk_method: str = "noaux_tc",
115
+ router_dtype: str = "fp32",
116
+ gated_attention_proj_granularity_type: str | None = None,
117
+ no_kda_lora: bool = False,
118
+ kda_safe_gate: bool = False,
119
+ kda_lower_bound: float | None = None,
120
+ short_conv_kernel_size: int = 4,
121
+ architectures: list[str] | None = None,
122
+ **kwargs: Any,
123
+ ) -> None:
124
+ kwargs.pop("model_type", None)
125
+ if architectures is None:
126
+ architectures = ["BailingMoeV3ForCausalLM"]
127
+
128
+ if rope_parameters is not None:
129
+ self.rope_parameters = dict(rope_parameters)
130
+ elif rope_scaling is not None:
131
+ self.rope_parameters = dict(rope_scaling)
132
+ else:
133
+ self.rope_parameters = {}
134
+ if (
135
+ "rope_type" not in self.rope_parameters
136
+ and "type" not in self.rope_parameters
137
+ ):
138
+ self.rope_parameters["rope_type"] = "default"
139
+ self.rope_parameters.setdefault("rope_theta", rope_theta)
140
+ if mrope_section is not None:
141
+ _normalize_mrope_parameters(self, list(mrope_section))
142
+
143
+ self.vocab_size = vocab_size
144
+ self.hidden_size = hidden_size
145
+ self.intermediate_size = intermediate_size
146
+ self.num_hidden_layers = num_hidden_layers
147
+ self.num_attention_heads = num_attention_heads
148
+ self.num_key_value_heads = num_key_value_heads
149
+ self.hidden_act = hidden_act
150
+ self.use_qkv_bias = use_qkv_bias
151
+ self.use_bias = use_bias
152
+ self.rms_norm_eps = rms_norm_eps
153
+ self.embedding_dropout = embedding_dropout
154
+ self.attention_dropout = attention_dropout
155
+ self.output_dropout = output_dropout
156
+ self.initializer_range = initializer_range
157
+ self.max_position_embeddings = max_position_embeddings
158
+ self.rope_theta = rope_theta
159
+ self.partial_rotary_factor = partial_rotary_factor
160
+ self.use_cache = use_cache
161
+ self.max_window_layers = max_window_layers
162
+ self.num_experts = num_experts
163
+ self.num_shared_experts = num_shared_experts
164
+ self.num_experts_per_tok = num_experts_per_tok
165
+ self.n_group = n_group
166
+ self.topk_group = topk_group
167
+ self.moe_intermediate_size = moe_intermediate_size
168
+ self.moe_shared_expert_intermediate_size = moe_shared_expert_intermediate_size
169
+ self.first_k_dense_replace = first_k_dense_replace
170
+ self.head_dim = head_dim or hidden_size // num_attention_heads
171
+ self.output_router_logits = output_router_logits
172
+ self.use_qk_norm = use_qk_norm
173
+ self.num_nextn_predict_layers = num_nextn_predict_layers
174
+ self.mtp_loss_scaling_factor = mtp_loss_scaling_factor
175
+ self.moe_router_enable_expert_bias = moe_router_enable_expert_bias
176
+ self.routed_scaling_factor = routed_scaling_factor
177
+ self.layer_group_size = layer_group_size
178
+ self.kv_lora_rank = kv_lora_rank
179
+ self.q_lora_rank = q_lora_rank
180
+ self.qk_rope_head_dim = qk_rope_head_dim
181
+ self.v_head_dim = v_head_dim
182
+ self.qk_nope_head_dim = qk_nope_head_dim
183
+ self.qk_head_dim = qk_nope_head_dim + qk_rope_head_dim
184
+ self.rope_interleave = rope_interleave
185
+ self.score_function = score_function
186
+ self.scoring_func = scoring_func
187
+ self.seq_aux = seq_aux
188
+ self.topk_method = topk_method
189
+ self.router_dtype = router_dtype
190
+ self.gated_attention_proj_granularity_type = (
191
+ gated_attention_proj_granularity_type
192
+ )
193
+ self.no_kda_lora = no_kda_lora
194
+ self.kda_safe_gate = kda_safe_gate
195
+ self.kda_lower_bound = kda_lower_bound
196
+ self.short_conv_kernel_size = short_conv_kernel_size
197
+
198
+ expected_layer_types = _build_layer_types(
199
+ num_hidden_layers,
200
+ layer_group_size,
201
+ )
202
+ if layers_block_type is not None:
203
+ legacy_type_mapping = {
204
+ "attention": "full_attention",
205
+ "mamba": "linear_attention",
206
+ "conv": "linear_attention",
207
+ }
208
+ normalized_block_types = [
209
+ legacy_type_mapping.get(layer_type, layer_type)
210
+ for layer_type in layers_block_type
211
+ ]
212
+ if layer_types is not None and layer_types != normalized_block_types:
213
+ raise ValueError(
214
+ "Bailing V3 VL has conflicting layer_types and layers_block_type"
215
+ )
216
+ layer_types = normalized_block_types
217
+ if layer_types is not None and layer_types != expected_layer_types:
218
+ raise ValueError(
219
+ "Bailing V3 VL layer_types must match the layer_group_size schedule"
220
+ )
221
+ self.layer_types = expected_layer_types
222
+
223
+ super().__init__(
224
+ architectures=architectures,
225
+ pad_token_id=pad_token_id,
226
+ eos_token_id=eos_token_id,
227
+ tie_word_embeddings=tie_word_embeddings,
228
+ **kwargs,
229
+ )
230
+ self.validate_layer_type()
231
+
232
+
233
+class BailingMoeV3VisionConfig(PretrainedConfig):
234
+ model_type = "qwen3_moe_vit"
235
+ base_config_key = "vision_config"
236
+
237
+ def __init__(
238
+ self,
239
+ *,
240
+ depth: int = 27,
241
+ hidden_size: int = 1152,
242
+ hidden_act: str = "gelu_pytorch_tanh",
243
+ intermediate_size: int = 4304,
244
+ num_heads: int = 16,
245
+ in_channels: int = 3,
246
+ patch_size: int = 16,
247
+ spatial_merge_size: int = 2,
248
+ temporal_patch_size: int = 2,
249
+ out_hidden_size: int = 3584,
250
+ num_position_embeddings: int = 2304,
251
+ deepstack_visual_indexes: list[int] | None = None,
252
+ initializer_range: float = 0.02,
253
+ **kwargs: Any,
254
+ ) -> None:
255
+ kwargs.pop("model_type", None)
256
+ super().__init__(**kwargs)
257
+ self.depth = depth
258
+ self.hidden_size = hidden_size
259
+ self.hidden_act = hidden_act
260
+ self.intermediate_size = intermediate_size
261
+ self.num_heads = num_heads
262
+ self.in_channels = in_channels
263
+ self.patch_size = patch_size
264
+ self.spatial_merge_size = spatial_merge_size
265
+ self.temporal_patch_size = temporal_patch_size
266
+ self.out_hidden_size = out_hidden_size
267
+ self.num_position_embeddings = num_position_embeddings
268
+ self.deepstack_visual_indexes = (
269
+ [8, 16, 24]
270
+ if deepstack_visual_indexes is None
271
+ else deepstack_visual_indexes
272
+ )
273
+ self.initializer_range = initializer_range
274
+
275
+
276
+class BailingMoeV3VLConfig(PretrainedConfig):
277
+ model_type = "bailing_moe_v3_vl"
278
+ keys_to_ignore_at_inference = ["past_key_values"]
279
+ sub_configs = {
280
+ "text_config": BailingMoeV3TextConfig,
281
+ "vision_config": BailingMoeV3VisionConfig,
282
+ }
283
+
284
+ def __init__(
285
+ self,
286
+ *,
287
+ text_config: BailingMoeV3TextConfig | dict[str, Any] | None = None,
288
+ vision_config: BailingMoeV3VisionConfig | dict[str, Any] | None = None,
289
+ image_token_id: int = 151655,
290
+ video_token_id: int = 151656,
291
+ vision_start_token_id: int = 151652,
292
+ vision_end_token_id: int = 151653,
293
+ tie_word_embeddings: bool = False,
294
+ mrope_section: list[int] | tuple[int, ...] | None = None,
295
+ architectures: list[str] | None = None,
296
+ **kwargs: Any,
297
+ ) -> None:
298
+ kwargs.pop("model_type", None)
299
+ if mrope_section is None:
300
+ mrope_section = [12, 10, 10]
301
+ self.mrope_section = list(mrope_section)
302
+
303
+ if text_config is None:
304
+ text_config = BailingMoeV3TextConfig()
305
+ elif isinstance(text_config, dict):
306
+ text_config = dict(text_config)
307
+ nested_mrope_section = text_config.pop("mrope_section", None)
308
+ if (
309
+ nested_mrope_section is not None
310
+ and list(nested_mrope_section) != self.mrope_section
311
+ ):
312
+ raise ValueError(
313
+ "Bailing V3 VL has conflicting M-RoPE sections: "
314
+ f"text={nested_mrope_section}, "
315
+ f"top-level={self.mrope_section}"
316
+ )
317
+ text_config = BailingMoeV3TextConfig(**text_config)
318
+ self.text_config = text_config
319
+ _normalize_mrope_parameters(self.text_config, self.mrope_section)
320
+
321
+ if vision_config is None:
322
+ vision_config = BailingMoeV3VisionConfig()
323
+ elif isinstance(vision_config, dict):
324
+ vision_config = BailingMoeV3VisionConfig(**vision_config)
325
+ self.vision_config = vision_config
326
+
327
+ self.image_token_id = image_token_id
328
+ self.video_token_id = video_token_id
329
+ self.vision_start_token_id = vision_start_token_id
330
+ self.vision_end_token_id = vision_end_token_id
331
+ if architectures is None:
332
+ architectures = ["BailingMoeV3VLForConditionalGeneration"]
333
+
334
+ super().__init__(
335
+ architectures=architectures,
336
+ tie_word_embeddings=tie_word_embeddings,
337
+ **kwargs,
338
+ )
339
+
340
+
341
+__all__ = [
342
+ "BailingMoeV3TextConfig",
343
+ "BailingMoeV3VisionConfig",
344
+ "BailingMoeV3VLConfig",
345
+]
vllm/v1/spec_decode/llm_base_proposer.py
+1
@@ -1356,6 +1356,7 @@ class SpecDecodeBaseProposer:
1356
# handle multimodality
1357
assert hasattr(target_model, "config")
1358
if self.get_model_name(target_model) in [
1359
+ "BailingMoeV3VLForConditionalGeneration",
1360
"Cohere2VisionForConditionalGeneration",
1361
"Exaone4_5_ForConditionalGeneration",
1362
"GlmOcrForConditionalGeneration",