A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 22:35 UTC
[Model] Add step3 vl (#21998)
Signed-off-by: oliveryuan <yuansong@step.ai> Co-authored-by: oliveryuan <yuansong@step.ai>
Song committed
Jul 31, 2025 at 23:19 UTC
94846416166c731939892350d7ab26dcbcb2982d
12 files changed
+2124
-1
docs/models/supported_models.md
+1
@@ -625,6 +625,7 @@ See [this page](generative_models.md) for more information on how to use generat
625
| `Qwen2_5OmniThinkerForConditionalGeneration` | Qwen2.5-Omni | T + I<sup>E+</sup> + V<sup>E+</sup> + A<sup>+</sup> | `Qwen/Qwen2.5-Omni-7B` | | ✅︎ | ✅︎ |
626
| `SkyworkR1VChatModel` | Skywork-R1V-38B | T + I | `Skywork/Skywork-R1V-38B` | | ✅︎ | ✅︎ |
627
| `SmolVLMForConditionalGeneration` | SmolVLM2 | T + I | `SmolVLM2-2.2B-Instruct` | ✅︎ | | ✅︎ |
628
+| `Step3VLForConditionalGeneration` | Step3-VL | T + I<sup>+</sup> | `stepfun-ai/step3` | | ✅︎ | ✅︎ |
629
| `TarsierForConditionalGeneration` | Tarsier | T + I<sup>E+</sup> | `omni-search/Tarsier-7b`, `omni-search/Tarsier-34b` | | ✅︎ | ✅︎ |
630
| `Tarsier2ForConditionalGeneration`<sup>^</sup> | Tarsier2 | T + I<sup>E+</sup> + V<sup>E+</sup> | `omni-research/Tarsier2-Recap-7b`, `omni-research/Tarsier2-7b-0115` | | ✅︎ | ✅︎ |
631
tests/models/registry.py
+6
@@ -279,6 +279,9 @@ _TEXT_GENERATION_EXAMPLE_MODELS = {
279
"StableLMEpochForCausalLM": _HfExamplesInfo("stabilityai/stablelm-zephyr-3b"), # noqa: E501
280
"StableLmForCausalLM": _HfExamplesInfo("stabilityai/stablelm-3b-4e1t"),
281
"Starcoder2ForCausalLM": _HfExamplesInfo("bigcode/starcoder2-3b"),
282
+ "Step3TextForCausalLM": _HfExamplesInfo("stepfun-ai/step3",
283
+ trust_remote_code=True,
284
+ is_available_online=False),
285
"SolarForCausalLM": _HfExamplesInfo("upstage/solar-pro-preview-instruct",
286
trust_remote_code=True),
287
"TeleChat2ForCausalLM": _HfExamplesInfo("Tele-AI/TeleChat2-3B",
@@ -457,6 +460,9 @@ _MULTIMODAL_EXAMPLE_MODELS = {
460
"SkyworkR1VChatModel": _HfExamplesInfo("Skywork/Skywork-R1V-38B",
461
trust_remote_code=True),
462
"SmolVLMForConditionalGeneration": _HfExamplesInfo("HuggingFaceTB/SmolVLM2-2.2B-Instruct"), # noqa: E501
463
+ "Step3VLForConditionalGeneration": _HfExamplesInfo("stepfun-ai/step3",
464
+ trust_remote_code=True,
465
+ is_available_online=False),
466
"UltravoxModel": _HfExamplesInfo("fixie-ai/ultravox-v0_5-llama-3_2-1b", # noqa: E501
467
trust_remote_code=True),
468
"TarsierForConditionalGeneration": _HfExamplesInfo("omni-research/Tarsier-7b", # noqa: E501
vllm/entrypoints/openai/tool_parsers/__init__.py
+2
@@ -18,6 +18,7 @@ from .mistral_tool_parser import MistralToolParser
18
from .phi4mini_tool_parser import Phi4MiniJsonToolParser
19
from .pythonic_tool_parser import PythonicToolParser
20
from .qwen3coder_tool_parser import Qwen3CoderToolParser
21
+from .step3_tool_parser import Step3ToolParser
22
from .xlam_tool_parser import xLAMToolParser
23
24
__all__ = [
@@ -40,4 +41,5 @@ __all__ = [
41
"HunyuanA13BToolParser",
42
"Glm4MoeModelToolParser",
43
"Qwen3CoderToolParser",
44
+ "Step3ToolParser",
45
]
vllm/entrypoints/openai/tool_parsers/step3_tool_parser.py
new
+296
@@ -0,0 +1,296 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import contextlib
5
+import json
6
+from collections.abc import Sequence
7
+from typing import Any, Optional, Union
8
+
9
+import regex as re
10
+
11
+from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
12
+ DeltaFunctionCall, DeltaMessage,
13
+ DeltaToolCall,
14
+ ExtractedToolCallInformation,
15
+ FunctionCall, ToolCall)
16
+from vllm.entrypoints.openai.tool_parsers.abstract_tool_parser import (
17
+ ToolParser, ToolParserManager)
18
+from vllm.logger import init_logger
19
+from vllm.transformers_utils.tokenizer import AnyTokenizer
20
+from vllm.utils import random_uuid
21
+
22
+logger = init_logger(__name__)
23
+
24
+
25
+@ToolParserManager.register_module(["step3"])
26
+class Step3ToolParser(ToolParser):
27
+ """
28
+ Tool parser for a model that uses a specific XML-like format for tool calls.
29
+ This version uses a robust, stateful, cursor-based streaming parser and
30
+ consolidates tool arguments into a single message.
31
+ """
32
+
33
+ TOOL_CALLS_BEGIN = "<|tool_calls_begin|>"
34
+ TOOL_CALLS_END = "<|tool_calls_end|>"
35
+ TOOL_CALL_BEGIN = "<|tool_call_begin|>"
36
+ TOOL_CALL_END = "<|tool_call_end|>"
37
+ TOOL_SEP = "<|tool_sep|>"
38
+ SPECIAL_TOKENS = [
39
+ TOOL_CALLS_BEGIN, TOOL_CALLS_END, TOOL_CALL_BEGIN, TOOL_CALL_END
40
+ ]
41
+
42
+ def __init__(self, tokenizer: AnyTokenizer):
43
+ super().__init__(tokenizer)
44
+ self.position = 0
45
+ # Explicit state flags for robust streaming
46
+ self.tool_block_started = False
47
+ self.tool_block_finished = False
48
+
49
+ def adjust_request(
50
+ self, request: ChatCompletionRequest) -> ChatCompletionRequest:
51
+ if request.tools and request.tool_choice != 'none':
52
+ request.skip_special_tokens = False
53
+ return request
54
+
55
+ @staticmethod
56
+ def _parse_steptml_invoke(
57
+ action_text: str
58
+ ) -> tuple[Optional[str], Optional[dict[str, str]]]:
59
+ func_name_match = re.search(r'<steptml:invoke name="([^"]+)">',
60
+ action_text)
61
+ if not func_name_match:
62
+ return None, None
63
+ func_name = func_name_match.group(1)
64
+
65
+ params: dict[str, str] = {}
66
+ param_matches = re.findall(
67
+ r'<steptml:parameter name="([^"]+)">([^<]*)</steptml:parameter>',
68
+ action_text)
69
+ for name, value in param_matches:
70
+ params[name] = value.strip()
71
+ return func_name, params
72
+
73
+ def _cast_arguments(
74
+ self,
75
+ func_name: str,
76
+ params: dict[str, Any],
77
+ request: ChatCompletionRequest,
78
+ ) -> dict[str, Any]:
79
+ for tool in request.tools or []:
80
+ if tool.function.name == func_name:
81
+ schema = tool.function.parameters or {}
82
+ properties = schema.get("properties", {})
83
+ for key, value in params.items():
84
+ if not isinstance(value, str):
85
+ continue
86
+ prop = properties.get(key, {})
87
+ typ = prop.get("type")
88
+ if typ == "string":
89
+ params[key] = value.strip()
90
+ elif typ == "integer":
91
+ with contextlib.suppress(ValueError):
92
+ params[key] = int(value)
93
+ elif typ == "number":
94
+ with contextlib.suppress(ValueError):
95
+ params[key] = float(value)
96
+ elif typ == "boolean":
97
+ lower_val = value.lower()
98
+ params[key] = lower_val == "true" if lower_val in (
99
+ "true", "false") else value
100
+ elif typ == "null":
101
+ params[key] = None if value.lower(
102
+ ) == "null" else value
103
+ break
104
+ return params
105
+
106
+ def extract_tool_calls_streaming(
107
+ self,
108
+ previous_text: str,
109
+ current_text: str,
110
+ delta_text: str,
111
+ previous_token_ids: Sequence[int],
112
+ current_token_ids: Sequence[int],
113
+ delta_token_ids: Sequence[int],
114
+ request: ChatCompletionRequest,
115
+ ) -> Union[DeltaMessage, None]:
116
+
117
+ # The main loop processes the stream from the last known position.
118
+ while True:
119
+ if self.position >= len(current_text):
120
+ return None # We've processed the entire stream.
121
+
122
+ unprocessed_text = current_text[self.position:]
123
+
124
+ # STATE: After all tools are done, all subsequent text is content.
125
+ if self.tool_block_finished:
126
+ self.position = len(current_text)
127
+ return DeltaMessage(content=unprocessed_text)
128
+
129
+ # STATE: Before the tool block has started.
130
+ if not self.tool_block_started:
131
+ if unprocessed_text.startswith(self.TOOL_CALLS_BEGIN):
132
+ self.position += len(self.TOOL_CALLS_BEGIN)
133
+ self.tool_block_started = True
134
+ continue # Token consumed, re-loop.
135
+
136
+ start_pos = unprocessed_text.find(self.TOOL_CALLS_BEGIN)
137
+ if start_pos == -1:
138
+ if self.TOOL_CALLS_BEGIN.startswith(
139
+ unprocessed_text.strip()) and unprocessed_text:
140
+ return None # It's a prefix, wait.
141
+ self.position = len(current_text)
142
+ return DeltaMessage(content=unprocessed_text)
143
+ else:
144
+ content = unprocessed_text[:start_pos]
145
+ self.position += len(content)
146
+ return DeltaMessage(content=content)
147
+
148
+ # STATE: Inside the main tool block.
149
+ offset = len(unprocessed_text) - len(unprocessed_text.lstrip())
150
+ unprocessed_text = unprocessed_text.lstrip()
151
+ self.position += offset
152
+
153
+ if unprocessed_text.startswith(self.TOOL_CALLS_END):
154
+ self.position += len(self.TOOL_CALLS_END)
155
+ self.tool_block_finished = True
156
+ self.current_tool_id = -1
157
+ continue
158
+
159
+ # Check if we are between tool calls.
160
+ tool_finished = (
161
+ self.current_tool_id != -1 and
162
+ self.prev_tool_call_arr[self.current_tool_id].get("finished"))
163
+ if self.current_tool_id == -1 or tool_finished:
164
+ if unprocessed_text.startswith(self.TOOL_CALL_BEGIN):
165
+ self.position += len(self.TOOL_CALL_BEGIN)
166
+ if self.current_tool_id == -1:
167
+ self.current_tool_id = 0
168
+ else:
169
+ self.current_tool_id += 1
170
+ self.current_tool_name_sent = False
171
+ while len(self.prev_tool_call_arr) <= self.current_tool_id:
172
+ self.prev_tool_call_arr.append({})
173
+ self.prev_tool_call_arr[
174
+ self.current_tool_id]["finished"] = False
175
+ continue
176
+
177
+ if self.TOOL_CALL_BEGIN.startswith(unprocessed_text):
178
+ return None
179
+
180
+ # STATE: Parsing an active tool call.
181
+ if self.current_tool_id != -1 and not self.prev_tool_call_arr[
182
+ self.current_tool_id].get("finished", False):
183
+ end_tool_pos = unprocessed_text.find(self.TOOL_CALL_END)
184
+ if end_tool_pos == -1:
185
+ tool_body = unprocessed_text
186
+ else:
187
+ tool_body = unprocessed_text[:end_tool_pos]
188
+
189
+ if end_tool_pos == -1 and self.TOOL_CALL_END.startswith(
190
+ tool_body):
191
+ return None
192
+
193
+ function_name, arguments = self._parse_steptml_invoke(
194
+ tool_body)
195
+ if not function_name:
196
+ return None
197
+
198
+ tool_call_arr = {
199
+ "name": function_name,
200
+ "parameters": arguments or {}
201
+ }
202
+
203
+ # Send the function name as soon as it's parsed.
204
+ if not self.current_tool_name_sent:
205
+ self.current_tool_name_sent = True
206
+ self.prev_tool_call_arr[self.current_tool_id].update(
207
+ tool_call_arr)
208
+ return DeltaMessage(tool_calls=[
209
+ DeltaToolCall(index=self.current_tool_id,
210
+ type="function",
211
+ id=f"chatcmpl-tool-{random_uuid()}",
212
+ function=DeltaFunctionCall(
213
+ name=function_name))
214
+ ])
215
+
216
+ # Update our internal state with the latest parsed arguments.
217
+ self.prev_tool_call_arr[
218
+ self.current_tool_id].update( # noqa: E501
219
+ tool_call_arr)
220
+
221
+ # Only send arguments when the tool call is complete.
222
+ if end_tool_pos != -1:
223
+ self.position += end_tool_pos + len(self.TOOL_CALL_END)
224
+ self.prev_tool_call_arr[
225
+ self.current_tool_id]["finished"] = True
226
+
227
+ final_args = self._cast_arguments(
228
+ function_name,
229
+ tool_call_arr.get("parameters", {}), # type: ignore
230
+ request)
231
+ if final_args:
232
+ final_args_json = json.dumps(final_args,
233
+ ensure_ascii=False)
234
+ return DeltaMessage(tool_calls=[
235
+ DeltaToolCall(index=self.current_tool_id,
236
+ function=DeltaFunctionCall(
237
+ arguments=final_args_json))
238
+ ])
239
+
240
+ # If tool is not finished, return None to wait for more tokens.
241
+ return None
242
+
243
+ return None
244
+
245
+ def extract_tool_calls(
246
+ self,
247
+ model_output: str,
248
+ request: ChatCompletionRequest,
249
+ ) -> ExtractedToolCallInformation:
250
+ if self.TOOL_CALLS_BEGIN not in model_output:
251
+ return ExtractedToolCallInformation(tools_called=False,
252
+ tool_calls=[],
253
+ content=model_output)
254
+
255
+ pre_text, rest = model_output.split(self.TOOL_CALLS_BEGIN, 1)
256
+ if self.TOOL_CALLS_END not in rest:
257
+ return ExtractedToolCallInformation(tools_called=False,
258
+ tool_calls=[],
259
+ content=model_output)
260
+
261
+ tool_block, post_text = rest.split(self.TOOL_CALLS_END, 1)
262
+ content = (pre_text + post_text).strip()
263
+
264
+ tool_calls: list[ToolCall] = []
265
+ call_parts = tool_block.split(self.TOOL_CALL_BEGIN)
266
+
267
+ for part in call_parts:
268
+ if not part or self.TOOL_CALL_END not in part:
269
+ continue
270
+
271
+ call_content = part.split(self.TOOL_CALL_END, 1)[0]
272
+ if self.TOOL_SEP not in call_content:
273
+ continue
274
+
275
+ type_part, invoke_part = call_content.split(self.TOOL_SEP, 1)
276
+ if type_part.strip() != "function":
277
+ continue
278
+
279
+ function_name, params_dict = self._parse_steptml_invoke(
280
+ invoke_part)
281
+
282
+ if function_name and params_dict is not None:
283
+ params_dict = self._cast_arguments(function_name, params_dict,
284
+ request)
285
+ params_str = json.dumps(params_dict, ensure_ascii=False)
286
+ tool_calls.append(
287
+ ToolCall(function=FunctionCall(name=function_name,
288
+ arguments=params_str)))
289
+ if tool_calls:
290
+ return ExtractedToolCallInformation(
291
+ tools_called=True,
292
+ tool_calls=tool_calls,
293
+ content=content if content else None)
294
+ return ExtractedToolCallInformation(tools_called=False,
295
+ tool_calls=[],
296
+ content=model_output)
vllm/model_executor/models/registry.py
+2
@@ -129,6 +129,7 @@ _TEXT_GENERATION_MODELS = {
129
"Qwen3ForCausalLM": ("qwen3", "Qwen3ForCausalLM"),
130
"Qwen3MoeForCausalLM": ("qwen3_moe", "Qwen3MoeForCausalLM"),
131
"RWForCausalLM": ("falcon", "FalconForCausalLM"),
132
+ "Step3TextForCausalLM": ("step3_text", "Step3TextForCausalLM"),
133
"StableLMEpochForCausalLM": ("stablelm", "StablelmForCausalLM"),
134
"StableLmForCausalLM": ("stablelm", "StablelmForCausalLM"),
135
"Starcoder2ForCausalLM": ("starcoder2", "Starcoder2ForCausalLM"),
@@ -238,6 +239,7 @@ _MULTIMODAL_MODELS = {
239
"Qwen2_5OmniModel": ("qwen2_5_omni_thinker", "Qwen2_5OmniThinkerForConditionalGeneration"), # noqa: E501
240
"Qwen2_5OmniForConditionalGeneration": ("qwen2_5_omni_thinker", "Qwen2_5OmniThinkerForConditionalGeneration"), # noqa: E501
241
"UltravoxModel": ("ultravox", "UltravoxModel"),
242
+ "Step3VLForConditionalGeneration": ("step3_vl", "Step3VLForConditionalGeneration"), # noqa: E501
243
"TarsierForConditionalGeneration": ("tarsier", "TarsierForConditionalGeneration"), # noqa: E501
244
"Tarsier2ForConditionalGeneration": ("qwen2_vl", "Tarsier2ForConditionalGeneration"), # noqa: E501
245
"VoxtralForConditionalGeneration": ("voxtral", "VoxtralForConditionalGeneration"), # noqa: E501
vllm/model_executor/models/step3_text.py
new
+521
@@ -0,0 +1,521 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Inference-only Jurassic model."""
4
+from collections.abc import Iterable
5
+from typing import Any, Optional
6
+
7
+import torch
8
+from torch import nn
9
+
10
+from vllm.attention import Attention
11
+from vllm.compilation.decorators import support_torch_compile
12
+from vllm.config import CacheConfig, ModelConfig, VllmConfig
13
+from vllm.distributed import (get_pp_group,
14
+ get_tensor_model_parallel_world_size,
15
+ tensor_model_parallel_all_reduce)
16
+from vllm.logger import init_logger
17
+from vllm.model_executor.layers.activation import SiluAndMul
18
+from vllm.model_executor.layers.fused_moe import FusedMoE
19
+from vllm.model_executor.layers.layernorm import RMSNorm
20
+from vllm.model_executor.layers.linear import (ColumnParallelLinear,
21
+ MergedColumnParallelLinear,
22
+ ReplicatedLinear,
23
+ RowParallelLinear)
24
+from vllm.model_executor.layers.logits_processor import LogitsProcessor
25
+from vllm.model_executor.layers.quantization.base_config import (
26
+ QuantizationConfig)
27
+from vllm.model_executor.layers.rotary_embedding import get_rope
28
+from vllm.model_executor.layers.sampler import SamplerOutput, get_sampler
29
+from vllm.model_executor.layers.vocab_parallel_embedding import (
30
+ DEFAULT_VOCAB_PADDING_SIZE, ParallelLMHead, VocabParallelEmbedding)
31
+from vllm.model_executor.model_loader.weight_utils import default_weight_loader
32
+from vllm.model_executor.sampling_metadata import SamplingMetadata
33
+from vllm.sequence import IntermediateTensors
34
+
35
+from .interfaces import SupportsPP
36
+from .utils import (PPMissingLayer, is_pp_missing_parameter,
37
+ make_empty_intermediate_tensors_factory, make_layers)
38
+
39
+logger = init_logger(__name__)
40
+
41
+
42
+class FusedMoEBlock(nn.Module):
43
+
44
+ def __init__(self,
45
+ config: ModelConfig,
46
+ quant_config: Optional[QuantizationConfig] = None,
47
+ prefix: str = ""):
48
+ super().__init__()
49
+ self.tp_size = get_tensor_model_parallel_world_size()
50
+
51
+ if self.tp_size > config.moe_num_experts:
52
+ raise ValueError(
53
+ f"Tensor parallel size {self.tp_size} is greater than "
54
+ f"the number of experts {config.moe_num_experts}.")
55
+
56
+ self.experts = FusedMoE(num_experts=config.moe_num_experts,
57
+ top_k=config.moe_top_k,
58
+ hidden_size=config.hidden_size,
59
+ intermediate_size=config.moe_intermediate_size,
60
+ reduce_results=False,
61
+ renormalize=config.norm_expert_weight,
62
+ quant_config=quant_config,
63
+ prefix=f"{prefix}.experts")
64
+ self.gate = ReplicatedLinear(config.hidden_size,
65
+ config.moe_num_experts,
66
+ bias=False,
67
+ quant_config=None,
68
+ prefix=f"{prefix}.gate")
69
+
70
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
71
+ orig_shape = hidden_states.shape
72
+ hidden_dim = hidden_states.shape[-1]
73
+ hidden_states = hidden_states.view(-1, hidden_dim)
74
+
75
+ router_logits, _ = self.gate(hidden_states)
76
+
77
+ final_hidden_states = self.experts(hidden_states=hidden_states,
78
+ router_logits=router_logits)
79
+ if self.tp_size > 1:
80
+ final_hidden_states = tensor_model_parallel_all_reduce(
81
+ final_hidden_states)
82
+
83
+ return final_hidden_states.view(orig_shape)
84
+
85
+
86
+class Step3TextMLP(nn.Module):
87
+
88
+ def __init__(
89
+ self,
90
+ hidden_size: int,
91
+ intermediate_size: int,
92
+ hidden_act: str,
93
+ quant_config: Optional[QuantizationConfig] = None,
94
+ prefix: str = "",
95
+ ) -> None:
96
+ super().__init__()
97
+ self.gate_up_proj = MergedColumnParallelLinear(
98
+ hidden_size, [intermediate_size] * 2,
99
+ bias=False,
100
+ quant_config=quant_config,
101
+ prefix=f"{prefix}.gate_up_proj")
102
+ self.down_proj = RowParallelLinear(intermediate_size,
103
+ hidden_size,
104
+ bias=False,
105
+ quant_config=quant_config,
106
+ prefix=f"{prefix}.down_proj")
107
+ if hidden_act != "silu":
108
+ raise ValueError(f"Unsupported activation: {hidden_act}. "
109
+ "Only silu is supported for now.")
110
+ self.act_fn = SiluAndMul()
111
+ self.hidden_size = hidden_size
112
+
113
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
114
+ gate_up, _ = self.gate_up_proj(hidden_states)
115
+ intermediate_act = self.act_fn(gate_up)
116
+ output, _ = self.down_proj(intermediate_act)
117
+ return output
118
+
119
+
120
+class Step3TextAttention(nn.Module):
121
+
122
+ def __init__(
123
+ self,
124
+ hidden_size: int,
125
+ num_heads: int,
126
+ num_kv_heads: int,
127
+ norm_eps: float,
128
+ rope_theta: int,
129
+ share_q_dim: Optional[int] = None,
130
+ rope_scaling: Optional[dict[str, Any]] = None,
131
+ max_position_embedding: int = 8192,
132
+ head_dim: int = 256,
133
+ cache_config: Optional[CacheConfig] = None,
134
+ quant_config: Optional[QuantizationConfig] = None,
135
+ prefix: str = "",
136
+ ):
137
+ super().__init__()
138
+ self.hidden_size = hidden_size
139
+ tp_size = get_tensor_model_parallel_world_size()
140
+
141
+ self.total_num_heads = num_heads
142
+ assert self.total_num_heads % tp_size == 0
143
+ self.num_heads = self.total_num_heads // tp_size
144
+
145
+ if num_kv_heads != 1:
146
+ raise ValueError(f"Step3TextAttention num_kv_heads must be 1, "
147
+ f"but got {num_kv_heads}.")
148
+ self.num_kv_heads = num_kv_heads
149
+
150
+ self.head_dim = head_dim
151
+ self.kv_size = self.num_kv_heads * self.head_dim
152
+ self.q_size = share_q_dim if share_q_dim else self.head_dim
153
+
154
+ self.qkv_proj = ReplicatedLinear(
155
+ hidden_size,
156
+ self.q_size + self.kv_size * 2,
157
+ bias=False,
158
+ quant_config=quant_config,
159
+ prefix=f"{prefix}.qkv_proj",
160
+ )
161
+
162
+ self.o_proj = RowParallelLinear(
163
+ self.total_num_heads * self.head_dim,
164
+ hidden_size,
165
+ bias=False,
166
+ quant_config=quant_config,
167
+ prefix=f"{prefix}.o_proj",
168
+ )
169
+ self.inter_norm = RMSNorm(self.q_size, eps=norm_eps)
170
+ self.wq = ColumnParallelLinear(
171
+ self.q_size,
172
+ self.head_dim * self.total_num_heads,
173
+ bias=False,
174
+ quant_config=quant_config,
175
+ prefix=f"{prefix}.wq",
176
+ )
177
+ self.rotary_emb = get_rope(self.head_dim,
178
+ rotary_dim=self.head_dim,
179
+ max_position=max_position_embedding,
180
+ base=rope_theta,
181
+ rope_scaling=rope_scaling)
182
+ scaling = self.head_dim**-0.5
183
+ self.attn = Attention(self.num_heads,
184
+ self.head_dim,
185
+ scaling,
186
+ self.num_kv_heads,
187
+ cache_config=cache_config,
188
+ prefix=f"{prefix}.attn")
189
+
190
+ def forward(self, positions: torch.Tensor,
191
+ hidden_states: torch.Tensor) -> torch.Tensor:
192
+ qkv, _ = self.qkv_proj(hidden_states)
193
+ q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
194
+ q = self.inter_norm(q)
195
+ q = self.wq(q)[0]
196
+ q, k = self.rotary_emb(positions, q, k)
197
+ attn_output = self.attn(q, k, v)
198
+ residual, _ = self.o_proj(attn_output)
199
+ return residual
200
+
201
+
202
+class Step3TextDecoderLayer(nn.Module):
203
+
204
+ def __init__(self,
205
+ config: ModelConfig,
206
+ cache_config: Optional[CacheConfig] = None,
207
+ quant_config: Optional[QuantizationConfig] = None,
208
+ prefix: str = "") -> None:
209
+ super().__init__()
210
+ config = config.hf_config
211
+ self.hidden_size = config.hidden_size
212
+ rope_scaling = getattr(config, "rope_scaling", None)
213
+
214
+ self.self_attn = Step3TextAttention(
215
+ hidden_size=self.hidden_size,
216
+ num_heads=config.num_attention_heads,
217
+ num_kv_heads=1,
218
+ cache_config=cache_config,
219
+ quant_config=quant_config,
220
+ norm_eps=config.rms_norm_eps,
221
+ max_position_embedding=config.max_position_embedding,
222
+ head_dim=config.head_dim,
223
+ share_q_dim=config.share_q_dim,
224
+ rope_theta=config.rope_theta,
225
+ rope_scaling=rope_scaling,
226
+ prefix=f"{prefix}.self_attn")
227
+
228
+ layer_idx = int(prefix.split("layers.")[1].split(".")[0])
229
+ moe_layers_enum = getattr(config, "moe_layers_enum", None)
230
+ if moe_layers_enum is not None:
231
+ moe_layers_idx = [
232
+ int(i) for i in moe_layers_enum.strip().split(',')
233
+ ]
234
+ else:
235
+ # Default to 1dense.
236
+ moe_layers_idx = [i for i in range(1, config.num_hidden_layers)]
237
+
238
+ if layer_idx in moe_layers_idx:
239
+ self.moe = FusedMoEBlock(config=config,
240
+ quant_config=quant_config,
241
+ prefix=f"{prefix}.moe")
242
+ self.share_expert = Step3TextMLP(
243
+ hidden_size=self.hidden_size,
244
+ intermediate_size=config.share_expert_dim,
245
+ hidden_act="silu",
246
+ quant_config=quant_config,
247
+ prefix=f"{prefix}.share_expert")
248
+ self.use_moe = True
249
+ else:
250
+ self.mlp = Step3TextMLP(hidden_size=config.hidden_size,
251
+ intermediate_size=config.intermediate_size,
252
+ hidden_act="silu",
253
+ quant_config=quant_config,
254
+ prefix=f"{prefix}.mlp")
255
+ self.use_moe = False
256
+ self.input_layernorm = RMSNorm(config.hidden_size,
257
+ eps=config.rms_norm_eps)
258
+ self.post_attention_layernorm = RMSNorm(config.hidden_size,
259
+ eps=config.rms_norm_eps)
260
+
261
+ def forward(
262
+ self, positions: torch.Tensor, hidden_states: torch.Tensor,
263
+ residual: Optional[torch.Tensor]
264
+ ) -> tuple[torch.Tensor, torch.Tensor]:
265
+ if residual is None:
266
+ residual = hidden_states
267
+ hidden_states = self.input_layernorm(hidden_states)
268
+ else:
269
+ hidden_states, residual = self.input_layernorm(
270
+ hidden_states, residual)
271
+
272
+ hidden_states = self.self_attn(
273
+ positions=positions,
274
+ hidden_states=hidden_states,
275
+ )
276
+
277
+ hidden_states, residual = self.post_attention_layernorm(
278
+ hidden_states, residual)
279
+
280
+ if self.use_moe:
281
+ share_output = self.share_expert(hidden_states)
282
+ moe_output = self.moe(hidden_states)
283
+ hidden_states = share_output + moe_output
284
+ else:
285
+ hidden_states = self.mlp(hidden_states)
286
+
287
+ return hidden_states, residual
288
+
289
+
290
+@support_torch_compile
291
+class Step3TextModel(nn.Module):
292
+
293
+ def __init__(self, vllm_config: VllmConfig, prefix: str = "") -> None:
294
+ super().__init__()
295
+ config = vllm_config.model_config.hf_config
296
+ cache_config = vllm_config.cache_config
297
+ quant_config = vllm_config.quant_config
298
+ self.vocab_size = config.vocab_size
299
+ self.config = config
300
+
301
+ if get_pp_group().is_first_rank or (config.tie_word_embeddings
302
+ and get_pp_group().is_last_rank):
303
+ self.embed_tokens = VocabParallelEmbedding(
304
+ self.vocab_size,
305
+ config.hidden_size,
306
+ )
307
+ else:
308
+ self.embed_tokens = PPMissingLayer()
309
+
310
+ self.start_layer, self.end_layer, self.layers = make_layers(
311
+ config.num_hidden_layers,
312
+ lambda prefix: Step3TextDecoderLayer(config=vllm_config.
313
+ model_config,
314
+ cache_config=cache_config,
315
+ quant_config=quant_config,
316
+ prefix=prefix),
317
+ prefix=f"{prefix}.layers",
318
+ )
319
+ if get_pp_group().is_last_rank:
320
+ self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
321
+ else:
322
+ self.norm = PPMissingLayer()
323
+
324
+ self.make_empty_intermediate_tensors = (
325
+ make_empty_intermediate_tensors_factory(["hidden_states"],
326
+ config.hidden_size))
327
+
328
+ def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
329
+ return self.embed_tokens(input_ids)
330
+
331
+ def forward(
332
+ self,
333
+ input_ids: torch.Tensor,
334
+ positions: torch.Tensor,
335
+ intermediate_tensors: Optional[IntermediateTensors] = None,
336
+ inputs_embeds: Optional[torch.Tensor] = None,
337
+ ) -> torch.Tensor:
338
+ if get_pp_group().is_first_rank:
339
+ if inputs_embeds is not None:
340
+ hidden_states = inputs_embeds
341
+ else:
342
+ hidden_states = self.get_input_embeddings(input_ids)
343
+ residual = None
344
+ else:
345
+ assert intermediate_tensors is not None
346
+ hidden_states = intermediate_tensors["hidden_states"]
347
+ residual = intermediate_tensors["residual"]
348
+
349
+ for i in range(self.start_layer, self.end_layer):
350
+ layer = self.layers[i]
351
+ hidden_states, residual = layer(positions, hidden_states, residual)
352
+
353
+ if not get_pp_group().is_last_rank:
354
+ return IntermediateTensors({
355
+ "hidden_states": hidden_states,
356
+ "residual": residual,
357
+ })
358
+
359
+ hidden_states, _ = self.norm(hidden_states, residual)
360
+ return hidden_states
361
+
362
+
363
+class Step3TextForCausalLM(nn.Module, SupportsPP):
364
+
365
+ def __init__(
366
+ self,
367
+ *,
368
+ vllm_config: VllmConfig,
369
+ prefix: str = "",
370
+ ):
371
+ super().__init__()
372
+ config = vllm_config.model_config.hf_config
373
+ lora_config = vllm_config.lora_config
374
+ self.config = config
375
+ self.vllm_config = vllm_config
376
+
377
+ self.model = Step3TextModel(vllm_config=vllm_config, prefix=prefix)
378
+
379
+ if get_pp_group().is_last_rank:
380
+ self.unpadded_vocab_size = config.vocab_size
381
+ if lora_config:
382
+ self.unpadded_vocab_size += lora_config.lora_extra_vocab_size
383
+ self.lm_head = ParallelLMHead(
384
+ self.unpadded_vocab_size,
385
+ config.hidden_size,
386
+ org_num_embeddings=config.vocab_size,
387
+ padding_size=DEFAULT_VOCAB_PADDING_SIZE
388
+ if not lora_config else lora_config.lora_vocab_padding_size,
389
+ )
390
+ self.logits_processor = LogitsProcessor(self.unpadded_vocab_size,
391
+ config.vocab_size)
392
+ self.sampler = get_sampler()
393
+ else:
394
+ self.lm_head = PPMissingLayer()
395
+
396
+ self.make_empty_intermediate_tensors = (
397
+ self.model.make_empty_intermediate_tensors)
398
+
399
+ def forward(self,
400
+ input_ids: torch.Tensor,
401
+ positions: torch.Tensor,
402
+ intermediate_tensors: Optional[IntermediateTensors] = None,
403
+ inputs_embeds: Optional[torch.Tensor] = None):
404
+ hidden_states = self.model(input_ids, positions, intermediate_tensors,
405
+ inputs_embeds)
406
+ return hidden_states
407
+
408
+ def compute_logits(self, hidden_states: torch.Tensor,
409
+ sampling_metadata: SamplingMetadata) -> torch.Tensor:
410
+ logits = self.logits_processor(self.lm_head, hidden_states,
411
+ sampling_metadata)
412
+ return logits
413
+
414
+ def sample(
415
+ self,
416
+ logits: Optional[torch.Tensor],
417
+ sampling_metadata: SamplingMetadata,
418
+ ) -> Optional[SamplerOutput]:
419
+ next_tokens = self.sampler(logits, sampling_metadata)
420
+ return next_tokens
421
+
422
+ def load_weights(self, weights: Iterable[tuple[str,
423
+ torch.Tensor]]) -> set[str]:
424
+ qkv_params_mapping = [
425
+ # (param_name, shard_name, relative_start_idx, relative_end_idx)
426
+ (".qkv_proj", ".q_proj", 0, self.config.share_q_dim /
427
+ (self.config.share_q_dim + self.config.head_dim * 2)),
428
+ (".qkv_proj", ".k_proj", self.config.share_q_dim /
429
+ (self.config.share_q_dim + self.config.head_dim * 2),
430
+ (self.config.share_q_dim + self.config.head_dim) /
431
+ (self.config.share_q_dim + self.config.head_dim * 2)),
432
+ (".qkv_proj", ".v_proj",
433
+ (self.config.share_q_dim + self.config.head_dim) /
434
+ (self.config.share_q_dim + self.config.head_dim * 2),
435
+ (self.config.share_q_dim + self.config.head_dim * 2) /
436
+ (self.config.share_q_dim + self.config.head_dim * 2)),
437
+ ]
438
+ stacked_params_mapping = [
439
+ # (param_name, shard_name, shard_id)
440
+ (".gate_up_proj", ".gate_proj", 0),
441
+ (".gate_up_proj", ".up_proj", 1),
442
+ ]
443
+ params_dict = dict(self.named_parameters())
444
+ loaded_params: set[str] = set()
445
+
446
+ expert_params_mapping = [
447
+ (".moe.experts.w13_weight", ".moe.gate_proj.weight", "w1"),
448
+ (".moe.experts.w13_weight", ".moe.up_proj.weight", "w3"),
449
+ (".moe.experts.w2_weight", ".moe.down_proj.weight", "w2")
450
+ ]
451
+
452
+ disable_moe_stacked_params = [
453
+ data[1] for data in expert_params_mapping
454
+ ]
455
+
456
+ for name, loaded_weight in weights:
457
+ for (param_name, weight_name, shard_id) in stacked_params_mapping:
458
+ if weight_name not in name:
459
+ continue
460
+ if any(disable_moe_stacked_param in name
461
+ for disable_moe_stacked_param in
462
+ disable_moe_stacked_params):
463
+ continue
464
+ name = name.replace(weight_name, param_name)
465
+ if is_pp_missing_parameter(name, self):
466
+ continue
467
+ param = params_dict[name]
468
+ weight_loader = param.weight_loader
469
+ weight_loader(param, loaded_weight, shard_id)
470
+ loaded_params.add(name)
471
+ break
472
+ else:
473
+ for mapping in expert_params_mapping:
474
+ param_name, weight_name, shard_id = mapping
475
+ if weight_name not in name:
476
+ continue
477
+ name = name.replace(weight_name, param_name)
478
+ # Skip layers on other devices.
479
+ if is_pp_missing_parameter(name, self):
480
+ continue
481
+ # Skip loading extra bias for GPTQ models.
482
+ if ((name.endswith(".bias") or name.endswith("_bias"))
483
+ and name not in params_dict):
484
+ continue
485
+ param = params_dict[name]
486
+ weight_loader = param.weight_loader
487
+ for expert_id in range(loaded_weight.shape[0]):
488
+ loaded_weight_expert = loaded_weight[expert_id]
489
+ weight_loader(param,
490
+ loaded_weight_expert,
491
+ name,
492
+ shard_id=shard_id,
493
+ expert_id=expert_id)
494
+ loaded_params.add(name)
495
+ break
496
+ else:
497
+ for (param_name, weight_name, start_idx,
498
+ end_idx) in qkv_params_mapping:
499
+ if weight_name not in name:
500
+ continue
501
+ name = name.replace(weight_name, param_name)
502
+ if is_pp_missing_parameter(name, self):
503
+ continue
504
+ param = params_dict[name]
505
+ dim = param.shape[param.output_dim]
506
+ begin_idx = int(start_idx * dim)
507
+ end_idx = int(end_idx * dim)
508
+ param_slice = param.narrow(param.output_dim, begin_idx,
509
+ end_idx - begin_idx)
510
+ param_slice.copy_(loaded_weight)
511
+ loaded_params.add(name)
512
+ break
513
+ else:
514
+ if is_pp_missing_parameter(name, self):
515
+ continue
516
+ param = params_dict[name]
517
+ weight_loader = getattr(param, "weight_loader",
518
+ default_weight_loader)
519
+ weight_loader(param, loaded_weight)
520
+ loaded_params.add(name)
521
+ return loaded_params
vllm/model_executor/models/step3_vl.py
new
+1052
@@ -0,0 +1,1052 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import math
4
+from collections.abc import Iterable, Mapping, Sequence
5
+from functools import cached_property
6
+from itertools import product
7
+from math import ceil, sqrt
8
+from typing import Any, Literal, Optional, TypedDict, Union
9
+
10
+import numpy as np
11
+import torch
12
+import torch.nn as nn
13
+import torch.nn.functional as F
14
+from PIL import Image
15
+from torchvision import transforms
16
+from torchvision.transforms.functional import InterpolationMode
17
+from transformers import BatchFeature, PretrainedConfig, TensorType
18
+
19
+from vllm.config import VllmConfig
20
+from vllm.distributed import get_tensor_model_parallel_world_size
21
+from vllm.model_executor.layers.activation import get_act_fn
22
+from vllm.model_executor.layers.linear import (ColumnParallelLinear,
23
+ QKVParallelLinear,
24
+ RowParallelLinear)
25
+from vllm.model_executor.layers.quantization import QuantizationConfig
26
+from vllm.model_executor.layers.sampler import SamplerOutput, get_sampler
27
+from vllm.model_executor.sampling_metadata import SamplingMetadata
28
+from vllm.multimodal import MULTIMODAL_REGISTRY
29
+from vllm.multimodal.inputs import (MultiModalDataDict, MultiModalFieldConfig,
30
+ MultiModalKwargs, NestedTensors)
31
+from vllm.multimodal.parse import ImageSize, MultiModalDataItems
32
+from vllm.multimodal.processing import (BaseMultiModalProcessor,
33
+ BaseProcessingInfo, PromptReplacement,
34
+ PromptUpdate, PromptUpdateDetails)
35
+from vllm.multimodal.profiling import BaseDummyInputsBuilder
36
+from vllm.sequence import IntermediateTensors
37
+from vllm.transformers_utils.configs import Step3VisionEncoderConfig
38
+from vllm.transformers_utils.tokenizer import AnyTokenizer
39
+
40
+from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
41
+from .utils import (AutoWeightsLoader, WeightsMapper, flatten_bn,
42
+ init_vllm_registered_model, maybe_prefix,
43
+ merge_multimodal_embeddings)
44
+
45
+
46
+class Step3VLImagePixelInputs(TypedDict):
47
+ type: Literal["pixel_values"]
48
+ pixel_values: torch.Tensor
49
+ patch_pixel_values: Optional[torch.Tensor]
50
+ num_patches: list[int]
51
+
52
+
53
+class Step3VLImageEmbeddingInputs(TypedDict):
54
+ type: Literal["image_embeds"]
55
+ image_embeds: torch.Tensor
56
+
57
+
58
+Step3VLImageInputs = Union[Step3VLImagePixelInputs,
59
+ Step3VLImageEmbeddingInputs]
60
+
61
+ImageWithPatches = tuple[Image.Image, list[Image.Image], list[int] | None]
62
+
63
+MAX_IMAGE_SIZE: int = 3024
64
+
65
+
66
+class Step3VisionProcessor:
67
+
68
+ def __init__(self, size, interpolation_mode="bicubic", patch_size=None):
69
+ mean = [0.48145466, 0.4578275, 0.40821073]
70
+ std = [0.26862954, 0.26130258, 0.27577711]
71
+ patch_size = patch_size if patch_size is not None else size
72
+
73
+ self.transform = transforms.Compose([
74
+ transforms.ToTensor(),
75
+ transforms.Normalize(mean, std),
76
+ transforms.Resize(
77
+ (size, size),
78
+ interpolation=InterpolationMode.BICUBIC if interpolation_mode
79
+ == "bicubic" else InterpolationMode.BILINEAR,
80
+ antialias=True),
81
+ ])
82
+
83
+ self.patch_transform = transforms.Compose([
84
+ transforms.ToTensor(),
85
+ transforms.Normalize(mean, std),
86
+ transforms.Resize(
87
+ (patch_size, patch_size),
88
+ interpolation=InterpolationMode.BICUBIC if interpolation_mode
89
+ == "bicubic" else InterpolationMode.BILINEAR,
90
+ antialias=True),
91
+ ]) if patch_size is not None else None
92
+
93
+ def __call__(self, image, is_patch=False):
94
+ if is_patch:
95
+ return {"pixel_values": self.patch_transform(image).unsqueeze(0)}
96
+ else:
97
+ return {"pixel_values": self.transform(image).unsqueeze(0)}
98
+
99
+
100
+class ImagePatcher:
101
+
102
+ def determine_window_size(self, long: int, short: int) -> int:
103
+ if long <= 728:
104
+ return short if long / short > 1.5 else 0
105
+ return min(short, 504) if long / short > 4 else 504
106
+
107
+ def slide_window(
108
+ self,
109
+ width: int,
110
+ height: int,
111
+ sizes: list[tuple[int, int]],
112
+ steps: list[tuple[int, int]],
113
+ img_rate_thr: float = 0.6,
114
+ ) -> tuple[list[tuple[int, int, int, int]], tuple[int, int]]:
115
+ assert 1 >= img_rate_thr >= 0, "The `in_rate_thr` should lie in 0~1"
116
+ windows = []
117
+ # Sliding windows.
118
+ for size, step in zip(sizes, steps):
119
+ size_w, size_h = size
120
+ step_w, step_h = step
121
+
122
+ x_num = 1 if width <= size_w else ceil((width - size_w) / step_w +
123
+ 1)
124
+ x_start = [step_w * i for i in range(x_num)]
125
+ if len(x_start) > 1 and x_start[-1] + size_w > width:
126
+ x_start[-1] = width - size_w
127
+
128
+ y_num = 1 if height <= size_h else ceil((height - size_h) /
129
+ step_h + 1)
130
+ y_start = [step_h * i for i in range(y_num)]
131
+ if len(y_start) > 1 and y_start[-1] + size_h > height:
132
+ y_start[-1] = height - size_h
133
+
134
+ start = np.array(list(product(y_start, x_start)), dtype=int)
135
+ start[:, [0, 1]] = start[:, [1, 0]]
136
+ windows.append(np.concatenate([start, start + size], axis=1))
137
+ windows = np.concatenate(windows, axis=0)
138
+
139
+ return [(int(box[0]), int(box[1]), int(box[2] - box[0]),
140
+ int(box[3] - box[1])) for box in windows], (x_num, y_num)
141
+
142
+ def square_pad(self, img: Image.Image) -> Image.Image:
143
+ w, h = img.size
144
+ if w == h:
145
+ return img
146
+ size = max(w, h)
147
+ padded = Image.new(img.mode, (size, size), 0)
148
+ padded.paste(img, (0, 0))
149
+ return padded
150
+
151
+ def get_image_size_for_padding(self, img_width: int,
152
+ img_height: int) -> tuple[int, int]:
153
+ ratio = img_width / img_height
154
+ if min(img_height, img_width) < 32 and (ratio > 4 or ratio < 1 / 4):
155
+ new_size = max(img_height, img_width)
156
+ return new_size, new_size
157
+ return img_width, img_height
158
+
159
+ def get_image_size_for_preprocess(self, img_width: int,
160
+ img_height: int) -> tuple[int, int]:
161
+
162
+ if max(img_height, img_width) > MAX_IMAGE_SIZE:
163
+ scale_factor = MAX_IMAGE_SIZE / max(img_height, img_width)
164
+ img_width = int(img_width * scale_factor)
165
+ img_height = int(img_height * scale_factor)
166
+ return img_width, img_height
167
+
168
+ def get_image_size_for_crop(self, img_width: int, img_height: int,
169
+ window_size: int):
170
+ w_ratio = img_width / window_size
171
+ h_ratio = img_height / window_size
172
+
173
+ if w_ratio < 1:
174
+ width_new = img_width
175
+ else:
176
+ decimal_w = w_ratio - img_width // window_size
177
+ w_ratio = int(w_ratio) + 1 if decimal_w > 0.2 else int(w_ratio)
178
+ width_new = window_size * w_ratio
179
+ if h_ratio < 1:
180
+ height_new = img_height
181
+ else:
182
+ decimal_h = h_ratio - img_height // window_size
183
+ h_ratio = int(h_ratio) + 1 if decimal_h > 0.2 else int(h_ratio)
184
+ height_new = window_size * h_ratio
185
+ return int(width_new), int(height_new)
186
+
187
+ def patch_crop(self, img: Image.Image, i: int, j: int, th: int, tw: int):
188
+ target = img.crop((j, i, j + tw, i + th))
189
+ return target
190
+
191
+ def get_num_patches(self, img_width: int,
192
+ img_height: int) -> tuple[int, int]:
193
+ img_width, img_height = self.get_image_size_for_padding(
194
+ img_width, img_height)
195
+ img_width, img_height = self.get_image_size_for_preprocess(
196
+ img_width, img_height)
197
+ window_size = self.determine_window_size(max(img_height, img_width),
198
+ min(img_height, img_width))
199
+ if window_size == 0:
200
+ return 0, 0
201
+ else:
202
+ img_width, img_height = self.get_image_size_for_crop(
203
+ img_width, img_height, window_size)
204
+ center_list, (x_num, y_num) = self.slide_window(
205
+ img_width, img_height, [(window_size, window_size)],
206
+ [(window_size, window_size)])
207
+ full_rows = (len(center_list) - 1) // x_num + 1
208
+ if len(center_list) > 0 and len(center_list) % x_num == 0:
209
+ full_rows -= 1
210
+ return len(center_list), full_rows
211
+
212
+ def __call__(
213
+ self, img: Image.Image
214
+ ) -> tuple[Image.Image, list[Image.Image], list[bool] | None]:
215
+ img_width, img_height = img.size
216
+ new_img_width, new_img_height = self.get_image_size_for_padding(
217
+ img_width, img_height)
218
+ if new_img_width != img_width or new_img_height != img_height:
219
+ img = self.square_pad(img)
220
+ img_width, img_height = img.size
221
+
222
+ new_img_width, new_img_height = self.get_image_size_for_preprocess(
223
+ img_width, img_height)
224
+ img = img.resize((new_img_width, new_img_height),
225
+ Image.Resampling.BILINEAR)
226
+ window_size = self.determine_window_size(
227
+ max(new_img_height, new_img_width),
228
+ min(new_img_height, new_img_width))
229
+
230
+ if window_size == 0:
231
+ return img, [], None
232
+ else:
233
+ new_img_width, new_img_height = self.get_image_size_for_crop(
234
+ new_img_width, new_img_height, window_size)
235
+ if (new_img_width, new_img_height) != (img_width, img_height):
236
+ img_for_crop = img.resize((new_img_width, new_img_height),
237
+ Image.Resampling.BILINEAR)
238
+ else:
239
+ img_for_crop = img
240
+
241
+ patches = []
242
+ newlines = []
243
+ center_list, (x_num, y_num) = self.slide_window(
244
+ new_img_width, new_img_height, [(window_size, window_size)],
245
+ [(window_size, window_size)])
246
+ for patch_id, center_lf_point in enumerate(center_list):
247
+ x, y, patch_w, patch_h = center_lf_point
248
+ big_patch = self.patch_crop(img_for_crop, y, x, patch_h,
249
+ patch_w)
250
+ patches.append(big_patch)
251
+ if (patch_id + 1) % x_num == 0:
252
+ newlines.append(patch_id)
253
+
254
+ if newlines and newlines[-1] == len(patches) - 1:
255
+ newlines.pop()
256
+
257
+ return img, patches, [i in newlines for i in range(len(patches))
258
+ ] if len(patches) > 0 else None
259
+
260
+
261
+class Step3VLProcessor:
262
+
263
+ def __init__(
264
+ self,
265
+ config: PretrainedConfig,
266
+ tokenizer: AnyTokenizer,
267
+ ) -> None:
268
+ super().__init__()
269
+
270
+ self.config = config
271
+ self.tokenizer = tokenizer
272
+
273
+ self.image_size = 728
274
+ self.patch_size = 504
275
+ self.image_preprocessor = Step3VisionProcessor(self.image_size,
276
+ "bilinear",
277
+ self.patch_size)
278
+
279
+ self.num_image_feature_size = 169
280
+ self.num_patch_feature_size = 81
281
+ self.image_token = "<im_patch>"
282
+ self.image_feature_placeholder = (self.image_token *
283
+ self.num_image_feature_size)
284
+ self.patch_feature_placeholder = (self.image_token *
285
+ self.num_patch_feature_size)
286
+
287
+ self.patcher = ImagePatcher()
288
+
289
+ @property
290
+ def image_token_id(self) -> int:
291
+ return self.tokenizer.get_vocab()[self.image_token]
292
+
293
+ def get_num_image_tokens(self, img_width: int, img_height: int) -> int:
294
+ num_patches, num_newlines = self.patcher.get_num_patches(
295
+ img_width, img_height)
296
+
297
+ return num_patches * (
298
+ self.num_patch_feature_size +
299
+ 2) + self.num_image_feature_size + 2 + num_newlines
300
+
301
+ def _split_images(self,
302
+ images: list[Image.Image]) -> list[ImageWithPatches]:
303
+ result = []
304
+ for img in images:
305
+ result.append(self.patcher(img))
306
+ return result
307
+
308
+ def _convert_images_to_pixel_values(
309
+ self,
310
+ images: list[Image.Image],
311
+ is_patch: bool = False,
312
+ ) -> list[torch.Tensor]:
313
+ return [
314
+ self.image_preprocessor(img, is_patch=is_patch)["pixel_values"]
315
+ for img in images
316
+ ]
317
+
318
+ def _get_patch_repl(
319
+ self,
320
+ num_patches: int,
321
+ patch_newline_mask: list[bool] | None,
322
+ ) -> tuple[str, list[int]]:
323
+ text = ""
324
+ token_ids = []
325
+ for i in range(num_patches):
326
+ assert len(patch_newline_mask) == num_patches
327
+ text += f"<patch_start>{self.patch_feature_placeholder}<patch_end>"
328
+ token_ids.extend(
329
+ [self.tokenizer.convert_tokens_to_ids("<patch_start>")] +
330
+ [self.image_token_id] * self.num_patch_feature_size +
331
+ [self.tokenizer.convert_tokens_to_ids("<patch_end>")])
332
+ if patch_newline_mask and patch_newline_mask[i]:
333
+ text += "<patch_newline>"
334
+ token_ids.append(
335
+ self.tokenizer.convert_tokens_to_ids("<patch_newline>"))
336
+ return text, token_ids
337
+
338
+ def _get_image_repl(
339
+ self,
340
+ num_images: int,
341
+ ) -> tuple[str, list[int]]:
342
+ text = f"<im_start>{self.image_feature_placeholder}<im_end>"
343
+ token_ids = [
344
+ self.tokenizer.convert_tokens_to_ids("<im_start>")
345
+ ] + [self.image_token_id] * self.num_image_feature_size + [
346
+ self.tokenizer.convert_tokens_to_ids("<im_end>")
347
+ ]
348
+ return text * num_images, token_ids * num_images
349
+
350
+ def _get_image_repl_features(
351
+ self,
352
+ num_images: int,
353
+ num_patches: int,
354
+ patch_new_line_idx: Optional[list[bool]],
355
+ ) -> tuple[str, list[int]]:
356
+ if num_patches > 0:
357
+ patch_repl, patch_repl_ids = self._get_patch_repl(
358
+ num_patches, patch_new_line_idx)
359
+ else:
360
+ patch_repl = ""
361
+ patch_repl_ids = []
362
+ image_repl, image_repl_ids = self._get_image_repl(num_images)
363
+ return patch_repl + image_repl, patch_repl_ids + image_repl_ids
364
+
365
+ def replace_placeholder(self, text: str, placeholder: str,
366
+ repls: list[str]) -> str:
367
+ parts = text.split(placeholder)
368
+
369
+ if len(parts) - 1 != len(repls):
370
+ raise ValueError(
371
+ "The number of placeholders does not match the number of replacements." # noqa: E501
372
+ )
373
+
374
+ result = [parts[0]]
375
+ for i, repl in enumerate(repls):
376
+ result.append(repl)
377
+ result.append(parts[i + 1])
378
+
379
+ return "".join(result)
380
+
381
+ def __call__(
382
+ self,
383
+ text: Optional[Union[str, list[str]]] = None,
384
+ images: Optional[Union[Image.Image, list[Image.Image]]] = None,
385
+ return_tensors: Optional[Union[str, TensorType]] = None,
386
+ ) -> BatchFeature:
387
+ if text is None:
388
+ text = []
389
+ if not isinstance(text, list):
390
+ text = [text]
391
+ if images is None:
392
+ images = []
393
+ if not isinstance(images, list):
394
+ images = [images]
395
+
396
+ if len(images) == 0:
397
+ image_inputs = {}
398
+ text_inputs = self.tokenizer(text)
399
+ else:
400
+ splitted_images_data = self._split_images(images)
401
+ pixel_values_lst = []
402
+ patch_pixel_values_lst = []
403
+ patch_newline_mask_lst = []
404
+ image_repl_str_lst = []
405
+ image_repl_ids_lst = []
406
+ num_patches = []
407
+ for raw_img, img_patches, patch_newline_mask in splitted_images_data: # noqa: E501
408
+ pixel_values_lst.extend(
409
+ self._convert_images_to_pixel_values([raw_img]))
410
+
411
+ if len(img_patches) > 0:
412
+ patch_pixel_values_lst.extend(
413
+ self._convert_images_to_pixel_values(img_patches,
414
+ is_patch=True))
415
+ num_patches.append(len(img_patches))
416
+
417
+ image_repl_str, image_repl_ids = self._get_image_repl_features(
418
+ 1, len(img_patches), patch_newline_mask)
419
+ image_repl_str_lst.append(image_repl_str)
420
+ image_repl_ids_lst.extend(image_repl_ids)
421
+
422
+ if patch_newline_mask is not None:
423
+ patch_newline_mask_lst.extend(patch_newline_mask)
424
+
425
+ image_inputs = {
426
+ "pixel_values": torch.cat(pixel_values_lst),
427
+ "num_patches": num_patches,
428
+ }
429
+ if patch_pixel_values_lst:
430
+ image_inputs["patch_pixel_values"] = torch.cat(
431
+ patch_pixel_values_lst)
432
+ if patch_newline_mask_lst:
433
+ image_inputs["patch_newline_mask"] = torch.tensor(
434
+ patch_newline_mask_lst, dtype=torch.bool)
435
+
436
+ text = [
437
+ self.replace_placeholder(t, self.image_token,
438
+ image_repl_str_lst) for t in text
439
+ ]
440
+ text_inputs = self.tokenizer(text)
441
+
442
+ return BatchFeature(
443
+ {
444
+ **text_inputs,
445
+ **image_inputs,
446
+ },
447
+ tensor_type=return_tensors,
448
+ )
449
+
450
+
451
+class Step3VLProcessingInfo(BaseProcessingInfo):
452
+
453
+ def get_hf_processor(self) -> Step3VLProcessor:
454
+ return Step3VLProcessor(
455
+ self.get_hf_config(),
456
+ self.get_tokenizer(),
457
+ )
458
+
459
+ def get_supported_mm_limits(self) -> Mapping[str, Optional[int]]:
460
+ return {"image": None}
461
+
462
+ def get_max_image_tokens(self) -> int:
463
+ hf_processor = self.get_hf_processor()
464
+ return hf_processor.get_num_image_tokens(
465
+ self.get_image_size_with_most_features().width,
466
+ self.get_image_size_with_most_features().height)
467
+
468
+ def get_mm_max_tokens_per_item(
469
+ self,
470
+ seq_len: int,
471
+ mm_counts: Mapping[str, int],
472
+ ) -> Mapping[str, int]:
473
+ return {"image": self.get_max_image_tokens()}
474
+
475
+ def get_image_size_with_most_features(self) -> ImageSize:
476
+ return ImageSize(3024, 3024)
477
+
478
+ def get_num_mm_tokens(self, mm_data: MultiModalDataDict) -> int:
479
+ if len(mm_data) != 1 or "image" not in mm_data:
480
+ raise ValueError(
481
+ "mm_data could only contain one key 'image' for steo1o")
482
+
483
+ image_data = mm_data["image"]
484
+ if not isinstance(image_data, (list, tuple)):
485
+ image_data = [image_data]
486
+
487
+ return sum(self.get_hf_processor().get_num_image_tokens(
488
+ img.width, img.height) for img in image_data)
489
+
490
+
491
+class Step3VLDummyInputsBuilder(BaseDummyInputsBuilder[Step3VLProcessingInfo]):
492
+
493
+ def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
494
+ num_images = mm_counts.get("image", 0)
495
+ return "<im_patch>" * num_images
496
+
497
+ def get_dummy_mm_data(
498
+ self,
499
+ seq_len: int,
500
+ mm_counts: Mapping[str, int],
501
+ ) -> MultiModalDataDict:
502
+ target_width, target_height = \
503
+ self.info.get_image_size_with_most_features()
504
+ num_images = mm_counts.get("image", 0)
505
+
506
+ return {
507
+ "image":
508
+ self._get_dummy_images(width=target_width,
509
+ height=target_height,
510
+ num_images=num_images)
511
+ }
512
+
513
+
514
+class Step3VLMultiModalProcessor(BaseMultiModalProcessor[Step3VLProcessingInfo]
515
+ ):
516
+
517
+ def _get_prompt_updates(
518
+ self,
519
+ mm_items: MultiModalDataItems,
520
+ hf_processor_mm_kwargs: Mapping[str, Any],
521
+ out_mm_kwargs: MultiModalKwargs,
522
+ ) -> Sequence[PromptUpdate]:
523
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
524
+ image_placeholder_token_id = hf_processor.image_token_id
525
+ batch_num_patches = out_mm_kwargs["num_patches"].tolist()
526
+
527
+ def get_replacement_step1o(item_idx: int):
528
+ img_out = out_mm_kwargs.get_item("image", item_idx)
529
+ num_patches = batch_num_patches[item_idx]
530
+ if num_patches > 0:
531
+ patch_newline_mask = img_out["patch_newline_mask"].data.tolist(
532
+ )
533
+ image_repl_ids = hf_processor._get_image_repl_features(
534
+ 1, num_patches, patch_newline_mask)[1]
535
+ else:
536
+ image_repl_ids = hf_processor._get_image_repl_features(
537
+ 1, 0, None)[1]
538
+ return PromptUpdateDetails.select_token_id(
539
+ seq=image_repl_ids,
540
+ embed_token_id=image_placeholder_token_id,
541
+ )
542
+
543
+ return [
544
+ PromptReplacement(
545
+ modality="image",
546
+ target=[image_placeholder_token_id],
547
+ replacement=get_replacement_step1o,
548
+ )
549
+ ]
550
+
551
+ def _get_mm_fields_config(
552
+ self,
553
+ hf_inputs: BatchFeature,
554
+ hf_processor_mm_kwargs: Mapping[str, object],
555
+ ) -> Mapping[str, MultiModalFieldConfig]:
556
+ num_patches = hf_inputs.get("num_patches", torch.empty(0))
557
+
558
+ return dict(
559
+ pixel_values=MultiModalFieldConfig.batched("image"),
560
+ patch_pixel_values=MultiModalFieldConfig.flat_from_sizes(
561
+ "image", num_patches),
562
+ num_patches=MultiModalFieldConfig.batched("image"),
563
+ patch_newline_mask=MultiModalFieldConfig.flat_from_sizes(
564
+ "image", num_patches),
565
+ )
566
+
567
+
568
+def get_abs_pos(abs_pos, tgt_size):
569
+ dim = abs_pos.size(-1)
570
+ abs_pos_new = abs_pos.squeeze(0)
571
+ cls_token, old_pos_embed = abs_pos_new[:1], abs_pos_new[1:]
572
+
573
+ src_size = int(math.sqrt(abs_pos_new.shape[0] - 1))
574
+ tgt_size = int(math.sqrt(tgt_size))
575
+ dtype = abs_pos.dtype
576
+
577
+ if src_size != tgt_size:
578
+ old_pos_embed = old_pos_embed.view(1, src_size, src_size,
579
+ dim).permute(0, 3, 1,
580
+ 2).contiguous()
581
+ old_pos_embed = old_pos_embed.to(torch.float32)
582
+ new_pos_embed = F.interpolate(
583
+ old_pos_embed,
584
+ size=(tgt_size, tgt_size),
585
+ mode='bicubic',
586
+ antialias=True,
587
+ align_corners=False,
588
+ ).to(dtype)
589
+ new_pos_embed = new_pos_embed.permute(0, 2, 3, 1)
590
+ new_pos_embed = new_pos_embed.view(tgt_size * tgt_size, dim)
591
+ vision_pos_embed = torch.cat([cls_token, new_pos_embed], dim=0)
592
+ vision_pos_embed = vision_pos_embed.view(1, tgt_size * tgt_size + 1,
593
+ dim)
594
+ return vision_pos_embed
595
+ else:
596
+ return abs_pos
597
+
598
+
599
+class Step3VisionEmbeddings(nn.Module):
600
+
601
+ def __init__(self, config: Step3VisionEncoderConfig):
602
+ super().__init__()
603
+ self.config = config
604
+ self.embed_dim = config.hidden_size
605
+ self.image_size = config.image_size
606
+ self.patch_size = config.patch_size
607
+
608
+ self.class_embedding = nn.Parameter(torch.randn(1, self.embed_dim))
609
+
610
+ self.patch_embedding = nn.Conv2d(
611
+ in_channels=config.num_channels,
612
+ out_channels=self.embed_dim,
613
+ kernel_size=self.patch_size,
614
+ stride=self.patch_size,
615
+ bias=True,
616
+ )
617
+
618
+ self.num_patches = (self.image_size // self.patch_size)**2
619
+ self.pad_tp_size = 4 # hard code for padding
620
+ # To load the pretrained weights, we still use P+1 as the seqlen
621
+ self.position_embedding = torch.nn.Embedding(self.num_patches + 1,
622
+ self.embed_dim)
623
+ self.register_buffer("position_ids",
624
+ torch.arange(self.num_patches + 1).expand(
625
+ (1, -1)),
626
+ persistent=False)
627
+
628
+ def forward(self, pixel_values: torch.Tensor) -> torch.Tensor:
629
+ batch_size = pixel_values.shape[0]
630
+ patch_embeds = self.patch_embedding(
631
+ pixel_values) # shape = [*, width, grid, grid]
632
+ patch_embeds = patch_embeds.flatten(2).transpose(1, 2)
633
+
634
+ # pad
635
+ class_embeds = self.class_embedding.expand(batch_size, 1, -1)
636
+ embeddings = torch.cat([class_embeds, patch_embeds], dim=1)
637
+ embeddings = embeddings + get_abs_pos(
638
+ self.position_embedding(self.position_ids), patch_embeds.size(1))
639
+ embeddings = torch.cat([
640
+ embeddings[:, 0, :].unsqueeze(1).repeat(1, self.pad_tp_size - 1,
641
+ 1), embeddings
642
+ ],
643
+ dim=1)
644
+ return embeddings
645
+
646
+
647
+class Step3VisionAttention(nn.Module):
648
+ """Multi-headed attention from 'Attention Is All You Need' paper"""
649
+
650
+ def __init__(self,
651
+ config,
652
+ quant_config: Optional[QuantizationConfig] = None,
653
+ prefix: str = ""):
654
+ super().__init__()
655
+ self.config = config
656
+ self.embed_dim = config.hidden_size
657
+ self.total_num_heads = config.num_attention_heads
658
+ self.head_dim = self.embed_dim // self.total_num_heads
659
+
660
+ self.scale = self.head_dim**-0.5
661
+
662
+ tp_size = get_tensor_model_parallel_world_size()
663
+ assert self.total_num_heads % tp_size == 0
664
+ self.num_heads = self.total_num_heads // tp_size
665
+ self.qkv_proj = QKVParallelLinear(self.embed_dim,
666
+ self.head_dim,
667
+ self.total_num_heads,
668
+ bias=True,
669
+ quant_config=quant_config,
670
+ prefix=prefix)
671
+ self.out_proj = RowParallelLinear(self.embed_dim,
672
+ self.embed_dim,
673
+ bias=True,
674
+ quant_config=quant_config,
675
+ prefix=prefix)
676
+
677
+ def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int):
678
+ return tensor.view(bsz, seq_len, self.num_heads,
679
+ self.head_dim).transpose(1, 2).contiguous()
680
+
681
+ def forward(
682
+ self,
683
+ hidden_states: torch.Tensor,
684
+ ):
685
+ """Input shape: Batch x Time x Channel"""
686
+ bsz, tgt_len, _ = hidden_states.size()
687
+
688
+ # get query proj
689
+ qkv, _ = self.qkv_proj(hidden_states)
690
+ q, k, v = qkv.chunk(chunks=3, dim=-1)
691
+ q = q.view(bsz, tgt_len, self.num_heads, self.head_dim)
692
+ k = k.view(bsz, tgt_len, self.num_heads, self.head_dim)
693
+ v = v.view(bsz, tgt_len, self.num_heads, self.head_dim)
694
+ q = q.transpose(1, 2)
695
+ k = k.transpose(1, 2)
696
+ v = v.transpose(1, 2)
697
+ attn_output = F.scaled_dot_product_attention(q,
698
+ k,
699
+ v,
700
+ scale=self.scale,
701
+ is_causal=False)
702
+ attn_output = attn_output.transpose(1, 2).reshape(
703
+ bsz, tgt_len, self.num_heads * self.head_dim)
704
+
705
+ attn_output, _ = self.out_proj(attn_output)
706
+
707
+ return attn_output
708
+
709
+
710
+class Step3VisionMLP(nn.Module):
711
+
712
+ def __init__(self,
713
+ config,
714
+ quant_config: Optional[QuantizationConfig] = None,
715
+ prefix: str = ""):
716
+ super().__init__()
717
+ self.config = config
718
+ self.activation_fn = get_act_fn(config.hidden_act)
719
+ self.fc1 = ColumnParallelLinear(config.hidden_size,
720
+ config.intermediate_size,
721
+ bias=True,
722
+ quant_config=quant_config,
723
+ prefix=prefix)
724
+ self.fc2 = RowParallelLinear(config.intermediate_size,
725
+ config.hidden_size,
726
+ bias=True,
727
+ quant_config=quant_config,
728
+ prefix=prefix)
729
+
730
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
731
+ hidden_states, _ = self.fc1(hidden_states)
732
+ hidden_states = self.activation_fn(hidden_states)
733
+ hidden_states, _ = self.fc2(hidden_states)
734
+ return hidden_states
735
+
736
+
737
+class Step3VisionEncoderLayer(nn.Module):
738
+
739
+ def __init__(self,
740
+ config: Step3VisionEncoderConfig,
741
+ quant_config: Optional[QuantizationConfig] = None,
742
+ prefix: str = ""):
743
+ super().__init__()
744
+ self.embed_dim = config.hidden_size
745
+ self.self_attn = Step3VisionAttention(config,
746
+ quant_config,
747
+ prefix=f"{prefix}.self_attn")
748
+ self.layer_norm1 = nn.LayerNorm(self.embed_dim,
749
+ eps=config.layer_norm_eps)
750
+ self.mlp = Step3VisionMLP(config, quant_config, prefix=f"{prefix}.mlp")
751
+ self.layer_norm2 = nn.LayerNorm(self.embed_dim,
752
+ eps=config.layer_norm_eps)
753
+
754
+ def forward(
755
+ self,
756
+ hidden_states: torch.Tensor,
757
+ ) -> torch.FloatTensor:
758
+ hidden_states = hidden_states + self.layer_norm1(
759
+ self.self_attn(hidden_states))
760
+ hidden_states = hidden_states + self.layer_norm2(
761
+ self.mlp(hidden_states))
762
+ return hidden_states
763
+
764
+
765
+class Step3VisionEncoder(nn.Module):
766
+
767
+ def __init__(self,
768
+ config: Step3VisionEncoderConfig,
769
+ quant_config: Optional[QuantizationConfig] = None,
770
+ prefix: str = ""):
771
+ super().__init__()
772
+ self.config = config
773
+ self.layers = nn.ModuleList([
774
+ Step3VisionEncoderLayer(config,
775
+ quant_config,
776
+ prefix=f"{prefix}.layers.{i}")
777
+ for i in range(config.num_hidden_layers)
778
+ ])
779
+
780
+ def forward(
781
+ self,
782
+ inputs_embeds,
783
+ ):
784
+ hidden_states = inputs_embeds
785
+ for encoder_layer in self.layers:
786
+ hidden_states = encoder_layer(hidden_states)
787
+ return hidden_states
788
+
789
+
790
+class Step3VisionTransformer(nn.Module):
791
+
792
+ def __init__(self,
793
+ config: Step3VisionEncoderConfig,
794
+ quant_config: Optional[QuantizationConfig] = None,
795
+ prefix: str = ""):
796
+ super().__init__()
797
+ self.config = config
798
+ self.image_size = config.image_size
799
+ self.embeddings = Step3VisionEmbeddings(config)
800
+ self.transformer = Step3VisionEncoder(config,
801
+ quant_config,
802
+ prefix=f"{prefix}.transformer")
803
+
804
+ def forward(
805
+ self,
806
+ pixel_values: torch.Tensor,
807
+ ):
808
+ hidden_states = self.embeddings(pixel_values)
809
+ hidden_states = self.transformer(inputs_embeds=hidden_states)
810
+ return hidden_states
811
+
812
+
813
+@MULTIMODAL_REGISTRY.register_processor(Step3VLMultiModalProcessor,
814
+ info=Step3VLProcessingInfo,
815
+ dummy_inputs=Step3VLDummyInputsBuilder)
816
+class Step3VLForConditionalGeneration(nn.Module, SupportsMultiModal,
817
+ SupportsPP):
818
+
819
+ hf_to_vllm_mapper = WeightsMapper(orig_to_new_prefix={
820
+ "model.": "language_model.model.",
821
+ "lm_head.": "language_model.lm_head.",
822
+ })
823
+
824
+ @classmethod
825
+ def get_placeholder_str(cls, modality: str, i: int) -> Optional[str]:
826
+ if modality.startswith("image"):
827
+ return "<im_patch>"
828
+
829
+ raise ValueError("Only image modality is supported")
830
+
831
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
832
+ super().__init__()
833
+
834
+ config = vllm_config.model_config.hf_config
835
+ multimodal_config = vllm_config.model_config.multimodal_config
836
+
837
+ self.config = config
838
+ self.multimodal_config = multimodal_config
839
+
840
+ self.vision_model = Step3VisionTransformer(config.vision_config,
841
+ None,
842
+ prefix=maybe_prefix(
843
+ prefix, "vision_model"))
844
+ self.vit_downsampler = nn.Conv2d(
845
+ config.vision_config.hidden_size,
846
+ config.vision_config.output_hidden_size,
847
+ kernel_size=2,
848
+ stride=config.understand_projector_stride)
849
+ self.vit_downsampler2 = nn.Conv2d(
850
+ config.vision_config.output_hidden_size,
851
+ config.vision_config.output_hidden_size * 2,
852
+ kernel_size=3,
853
+ stride=2,
854
+ padding=1,
855
+ )
856
+ self.vit_large_projector = nn.Linear(
857
+ config.vision_config.output_hidden_size * 2,
858
+ config.hidden_size,
859
+ bias=config.projector_bias,
860
+ )
861
+ self.language_model = init_vllm_registered_model(
862
+ vllm_config=vllm_config,
863
+ hf_config=config.text_config,
864
+ prefix=maybe_prefix(prefix, "language_model"))
865
+
866
+ self.make_empty_intermediate_tensors = (
867
+ self.language_model.make_empty_intermediate_tensors)
868
+
869
+ @cached_property
870
+ def sampler(self):
871
+ if hasattr(self.language_model, "sampler"):
872
+ return self.language_model.sampler
873
+
874
+ return get_sampler()
875
+
876
+ @property
877
+ def device(self):
878
+ return next(self.parameters()).device
879
+
880
+ @property
881
+ def dtype(self):
882
+ return next(self.parameters()).dtype
883
+
884
+ def _parse_and_validate_image_input(
885
+ self, **kwargs: object) -> Optional[Step3VLImageInputs]:
886
+ pixel_values = kwargs.pop("pixel_values", None)
887
+ patch_pixel_values = kwargs.pop("patch_pixel_values", None)
888
+ num_patches = kwargs.pop("num_patches", None)
889
+ image_embeds = kwargs.pop("image_embeds", None)
890
+
891
+ if pixel_values is None and image_embeds is None:
892
+ return None
893
+
894
+ if pixel_values is not None:
895
+ pixel_values = flatten_bn(pixel_values, concat=True)
896
+ if pixel_values.dim() >= 3:
897
+ pixel_values = pixel_values.view(-1, *pixel_values.shape[-3:])
898
+ if patch_pixel_values is not None:
899
+ patch_pixel_values = flatten_bn(patch_pixel_values,
900
+ concat=True)
901
+ patch_pixel_values = patch_pixel_values.view(
902
+ -1, *patch_pixel_values.shape[-3:])
903
+ # Handle empty patch_pixel_values by setting to None
904
+ if patch_pixel_values.shape[0] == 0:
905
+ patch_pixel_values = None
906
+ num_patches = flatten_bn(num_patches, concat=True).tolist()
907
+
908
+ return Step3VLImagePixelInputs(
909
+ type="pixel_values",
910
+ pixel_values=pixel_values.to(self.dtype).to(self.device),
911
+ patch_pixel_values=patch_pixel_values.to(self.dtype).to(
912
+ self.device) if patch_pixel_values is not None else None,
913
+ num_patches=num_patches,
914
+ )
915
+
916
+ if image_embeds is not None:
917
+ if image_embeds.dim() == 2 or image_embeds.dim() >= 3:
918
+ image_embeds = image_embeds.view(-1, image_embeds.shape[-1])
919
+ else:
920
+ raise ValueError(
921
+ f"Unexpected shape for image_embeds: {image_embeds.shape}")
922
+
923
+ return Step3VLImageEmbeddingInputs(
924
+ type="image_embeds",
925
+ image_embeds=image_embeds.to(self.dtype).to(self.device),
926
+ )
927
+ return None
928
+
929
+ def _process_image_features(self,
930
+ image_features: torch.Tensor) -> torch.Tensor:
931
+ B, P = image_features.shape[:2]
932
+ HW = int(sqrt(P))
933
+ image_features = image_features.permute(0, 2, 1).view(B, -1, HW, HW)
934
+ image_features = self.vit_downsampler(image_features)
935
+ image_features = self.vit_downsampler2(image_features)
936
+ n_dim = image_features.size(1)
937
+ image_features = image_features.view(B, n_dim, -1).permute(0, 2, 1)
938
+ image_features = self.vit_large_projector(image_features)
939
+ return image_features
940
+
941
+ def _get_vision_model_output(self,
942
+ input_tensor: torch.Tensor) -> torch.Tensor:
943
+ return self.vision_model(input_tensor)[:, 4:]
944
+
945
+ def _process_image_input(
946
+ self, image_input: Step3VLImageInputs) -> tuple[torch.Tensor, ...]:
947
+
948
+ if image_input["type"] == "image_embeds":
949
+ image_features = image_input["image_embeds"]
950
+ else:
951
+ image_features = self._get_vision_model_output(
952
+ image_input["pixel_values"])
953
+ patch_image_features = self._get_vision_model_output(
954
+ image_input["patch_pixel_values"]
955
+ ) if image_input["patch_pixel_values"] is not None else None
956
+ num_patches = image_input["num_patches"]
957
+
958
+ image_features = self._process_image_features(image_features)
959
+ patch_image_features = self._process_image_features(
960
+ patch_image_features) if patch_image_features is not None else None
961
+
962
+ merged_image_features = []
963
+ cur_patch_idx = 0
964
+ for i, num_patch in enumerate(num_patches):
965
+ cur_feature = []
966
+ if num_patch > 0:
967
+ patch_slice = patch_image_features[
968
+ cur_patch_idx:cur_patch_idx + num_patch]
969
+ cur_feature.append(patch_slice.view(-1, patch_slice.shape[-1]))
970
+ cur_feature.append(image_features[i].view(
971
+ -1, image_features.shape[-1]))
972
+ cur_patch_idx += num_patch
973
+ merged_image_features.append(
974
+ torch.cat(cur_feature) if len(cur_feature) >
975
+ 1 else cur_feature[0])
976
+ return merged_image_features
977
+
978
+ def get_multimodal_embeddings(self, **kwargs) -> Optional[NestedTensors]:
979
+ image_input = self._parse_and_validate_image_input(**kwargs)
980
+ if image_input is None:
981
+ return None
982
+ vision_embeddings = self._process_image_input(image_input)
983
+ return vision_embeddings
984
+
985
+ def get_input_embeddings(
986
+ self,
987
+ input_ids: torch.Tensor,
988
+ multimodal_embeddings: Optional[MultiModalEmbeddings] = None,
989
+ ) -> torch.Tensor:
990
+ if multimodal_embeddings is None:
991
+ inputs_embeds = self.language_model.model.get_input_embeddings(
992
+ input_ids)
993
+ else:
994
+ is_text = input_ids != self.config.image_token_id
995
+ text_ids = input_ids[is_text]
996
+ text_embeds = self.language_model.model.get_input_embeddings(
997
+ text_ids)
998
+ inputs_embeds = torch.empty(input_ids.shape[0],
999
+ text_embeds.shape[-1],
1000
+ dtype=text_embeds.dtype,
1001
+ device=text_embeds.device)
1002
+ inputs_embeds[is_text] = text_embeds
1003
+ inputs_embeds = merge_multimodal_embeddings(
1004
+ input_ids, inputs_embeds, multimodal_embeddings,
1005
+ self.config.image_token_id)
1006
+ return inputs_embeds
1007
+
1008
+ def forward(
1009
+ self,
1010
+ input_ids: torch.Tensor,
1011
+ positions: torch.Tensor,
1012
+ intermediate_tensors: Optional[IntermediateTensors] = None,
1013
+ inputs_embeds: Optional[torch.Tensor] = None,
1014
+ **kwargs: object,
1015
+ ) -> Union[torch.Tensor, IntermediateTensors]:
1016
+ if intermediate_tensors is not None:
1017
+ inputs_embeds = None
1018
+ elif inputs_embeds is None:
1019
+ vision_embeddings = self.get_multimodal_embeddings(**kwargs)
1020
+ # always pass the input via `inputs_embeds`
1021
+ # to make sure the computation graph is consistent
1022
+ inputs_embeds = self.get_input_embeddings(input_ids,
1023
+ vision_embeddings)
1024
+ input_ids = None
1025
+
1026
+ hidden_states = self.language_model(input_ids,
1027
+ positions,
1028
+ intermediate_tensors,
1029
+ inputs_embeds=inputs_embeds)
1030
+
1031
+ return hidden_states
1032
+
1033
+ def compute_logits(
1034
+ self,
1035
+ hidden_states: torch.Tensor,
1036
+ sampling_metadata: SamplingMetadata,
1037
+ ) -> Optional[torch.Tensor]:
1038
+ return self.language_model.compute_logits(hidden_states,
1039
+ sampling_metadata)
1040
+
1041
+ def sample(
1042
+ self,
1043
+ logits: torch.Tensor,
1044
+ sampling_metadata: SamplingMetadata,
1045
+ ) -> Optional[SamplerOutput]:
1046
+ return self.language_model.sample(logits, sampling_metadata)
1047
+
1048
+ def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
1049
+ loader = AutoWeightsLoader(self)
1050
+ loaded_weights = loader.load_weights(weights,
1051
+ mapper=self.hf_to_vllm_mapper)
1052
+ return loaded_weights
vllm/reasoning/__init__.py
+2
@@ -8,6 +8,7 @@ from .granite_reasoning_parser import GraniteReasoningParser
8
from .hunyuan_a13b_reasoning_parser import HunyuanA13BReasoningParser
9
from .mistral_reasoning_parser import MistralReasoningParser
10
from .qwen3_reasoning_parser import Qwen3ReasoningParser
11
+from .step3_reasoning_parser import Step3ReasoningParser
12
13
__all__ = [
14
"ReasoningParser",
@@ -18,4 +19,5 @@ __all__ = [
19
"Qwen3ReasoningParser",
20
"Glm4MoeModelReasoningParser",
21
"MistralReasoningParser",
22
+ "Step3ReasoningParser",
23
]
vllm/reasoning/step3_reasoning_parser.py
new
+109
@@ -0,0 +1,109 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from collections.abc import Sequence
5
+from typing import Optional, Union
6
+
7
+import regex as re
8
+from transformers import PreTrainedTokenizerBase
9
+
10
+from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
11
+ DeltaMessage)
12
+from vllm.logger import init_logger
13
+from vllm.reasoning import ReasoningParser, ReasoningParserManager
14
+
15
+logger = init_logger(__name__)
16
+
17
+
18
+@ReasoningParserManager.register_module("step3")
19
+class Step3ReasoningParser(ReasoningParser):
20
+ """
21
+ Reasoning parser for Step3 model.
22
+
23
+ The Step3 model uses </think> token to denote the end of reasoning
24
+ text. This parser extracts all content before </think> as reasoning content.
25
+ """
26
+
27
+ def __init__(self, tokenizer: PreTrainedTokenizerBase):
28
+ super().__init__(tokenizer)
29
+ self.think_end_token = "</think>"
30
+
31
+ self.reasoning_regex = re.compile(rf"(.*?){self.think_end_token}",
32
+ re.DOTALL)
33
+
34
+ if not self.model_tokenizer:
35
+ raise ValueError(
36
+ "The model tokenizer must be passed to the ReasoningParser "
37
+ "constructor during construction.")
38
+
39
+ self.think_end_token_id = self.vocab.get(self.think_end_token)
40
+ if self.think_end_token_id is None:
41
+ raise RuntimeError(
42
+ "Step3 reasoning parser could not locate think end "
43
+ "token in the tokenizer!")
44
+
45
+ def extract_reasoning_content_streaming(
46
+ self,
47
+ previous_text: str,
48
+ current_text: str,
49
+ delta_text: str,
50
+ previous_token_ids: Sequence[int],
51
+ current_token_ids: Sequence[int],
52
+ delta_token_ids: Sequence[int],
53
+ ) -> Union[DeltaMessage, None]:
54
+ """
55
+ Extract reasoning content from a delta message.
56
+ Handles streaming output where previous + delta = current.
57
+ Uses token IDs for faster processing.
58
+ For text "abc</think>xyz":
59
+ - 'abc' goes to reasoning_content
60
+ - 'xyz' goes to content
61
+ """
62
+ # Skip single special token
63
+ if len(delta_token_ids
64
+ ) == 1 and delta_token_ids[0] == self.think_end_token_id:
65
+ return None
66
+
67
+ if self.think_end_token_id in delta_token_ids:
68
+ # </think> in delta, extract reasoning content and remaining content
69
+ end_index = delta_text.find(self.think_end_token)
70
+ reasoning_content = delta_text[:end_index]
71
+ content = delta_text[end_index + len(self.think_end_token):]
72
+ return DeltaMessage(reasoning_content=reasoning_content,
73
+ content=content if content else None)
74
+ elif self.think_end_token_id in previous_token_ids:
75
+ # </think> already seen in previous text, everything is content
76
+ return DeltaMessage(content=delta_text)
77
+ else:
78
+ # No </think> seen yet, everything is reasoning
79
+ return DeltaMessage(reasoning_content=delta_text)
80
+
81
+ def extract_reasoning_content(
82
+ self, model_output: str, request: ChatCompletionRequest
83
+ ) -> tuple[Optional[str], Optional[str]]:
84
+
85
+ # Check if the model output contains the </think> token
86
+ if self.think_end_token not in model_output:
87
+ # If no </think> token, everything is reasoning content
88
+ return model_output, None
89
+ else:
90
+ # Find the first occurrence of </think>
91
+ end_index = model_output.find(self.think_end_token)
92
+ reasoning_content = model_output[:end_index]
93
+
94
+ # Content after </think> token
95
+ content = model_output[end_index + len(self.think_end_token):]
96
+
97
+ if len(content) == 0:
98
+ content = None
99
+
100
+ return reasoning_content, content
101
+
102
+ def is_reasoning_end(self, input_ids: list[int]) -> bool:
103
+ return self.think_end_token_id in input_ids
104
+
105
+ def extract_content_ids(self, input_ids: list[int]) -> list[int]:
106
+ if self.think_end_token_id not in input_ids[:-1]:
107
+ return []
108
+ else:
109
+ return input_ids[input_ids.index(self.think_end_token_id) + 1:]
vllm/transformers_utils/config.py
+4
-1
@@ -35,7 +35,8 @@ from vllm.transformers_utils.configs import (ChatGLMConfig, DeepseekVLV2Config,
35
MllamaConfig, MLPSpeculatorConfig,
36
Nemotron_Nano_VL_Config,
37
NemotronConfig, NVLM_D_Config,
38
- RWConfig, UltravoxConfig)
38
+ RWConfig, Step3TextConfig,
39
+ Step3VLConfig, UltravoxConfig)
40
# yapf: enable
41
from vllm.transformers_utils.configs.mistral import adapt_config_dict
42
from vllm.transformers_utils.utils import check_gguf_file
@@ -83,6 +84,8 @@ _CONFIG_REGISTRY: dict[str, type[PretrainedConfig]] = {
84
"nemotron": NemotronConfig,
85
"NVLM_D": NVLM_D_Config,
86
"ultravox": UltravoxConfig,
87
+ "step3_vl": Step3VLConfig,
88
+ "step3_text": Step3TextConfig,
89
**_CONFIG_REGISTRY_OVERRIDE_HF
90
}
91
vllm/transformers_utils/configs/__init__.py
+6
@@ -24,6 +24,9 @@ from vllm.transformers_utils.configs.nemotron import NemotronConfig
24
from vllm.transformers_utils.configs.nemotron_h import NemotronHConfig
25
from vllm.transformers_utils.configs.nemotron_vl import Nemotron_Nano_VL_Config
26
from vllm.transformers_utils.configs.nvlm_d import NVLM_D_Config
27
+from vllm.transformers_utils.configs.step3_vl import (Step3TextConfig,
28
+ Step3VisionEncoderConfig,
29
+ Step3VLConfig)
30
from vllm.transformers_utils.configs.ultravox import UltravoxConfig
31
32
__all__ = [
@@ -42,4 +45,7 @@ __all__ = [
45
"Nemotron_Nano_VL_Config",
46
"NVLM_D_Config",
47
"UltravoxConfig",
48
+ "Step3VLConfig",
49
+ "Step3VisionEncoderConfig",
50
+ "Step3TextConfig",
51
]
vllm/transformers_utils/configs/step3_vl.py
new
+123
@@ -0,0 +1,123 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+from typing import Any, Optional, Union
4
+
5
+from transformers.configuration_utils import PretrainedConfig
6
+
7
+
8
+class Step3VisionEncoderConfig(PretrainedConfig):
9
+ model_type = "step3_vision_encoder"
10
+
11
+ def __init__(
12
+ self,
13
+ hidden_size=1792,
14
+ intermediate_size=3072,
15
+ output_hidden_size=4096,
16
+ num_hidden_layers=63,
17
+ num_attention_heads=16,
18
+ num_channels=3,
19
+ image_size=728,
20
+ patch_size=14,
21
+ hidden_act="quick_gelu",
22
+ layer_norm_eps=1e-5,
23
+ **kwargs,
24
+ ):
25
+ self.hidden_size = hidden_size
26
+ self.intermediate_size = intermediate_size
27
+ self.output_hidden_size = output_hidden_size
28
+ self.num_hidden_layers = num_hidden_layers
29
+ self.num_attention_heads = num_attention_heads
30
+ self.num_channels = num_channels
31
+ self.patch_size = patch_size
32
+ self.image_size = image_size
33
+ self.layer_norm_eps = layer_norm_eps
34
+ self.hidden_act = hidden_act
35
+ super().__init__(**kwargs)
36
+
37
+
38
+class Step3TextConfig(PretrainedConfig):
39
+ model_type = "step3_text"
40
+ architectures = ["Step3TextForCausalLM"]
41
+
42
+ def __init__(
43
+ self,
44
+ hidden_size: int = 7168,
45
+ intermediate_size: int = 18432,
46
+ num_attention_heads: int = 64,
47
+ num_attention_groups: int = 1,
48
+ num_hidden_layers: int = 61,
49
+ max_seq_len: int = 65536,
50
+ vocab_size: int = 128815,
51
+ rms_norm_eps: float = 1e-5,
52
+ moe_intermediate_size: int = 5120,
53
+ moe_num_experts: int = 48,
54
+ moe_top_k: int = 3,
55
+ rope_theta: float = 500000,
56
+ rope_scaling: Optional[dict[str, Any]] = None,
57
+ max_position_embedding: int = 65536,
58
+ share_expert_dim: int = 5120,
59
+ share_q_dim: int = 2048,
60
+ head_dim: int = 256,
61
+ norm_expert_weight: bool = False,
62
+ moe_layers_enum: tuple[int,
63
+ ...] = (4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
64
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
65
+ 25, 26, 27, 28, 29, 30, 31, 32, 33, 34,
66
+ 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
67
+ 45, 46, 47, 48, 49, 50, 51, 52, 53, 54,
68
+ 55, 56, 57, 58, 59),
69
+ **kwargs,
70
+ ) -> None:
71
+ self.hidden_size = hidden_size
72
+ self.intermediate_size = intermediate_size
73
+ self.num_attention_heads = num_attention_heads
74
+ self.num_attention_groups = num_attention_groups
75
+ self.num_hidden_layers = num_hidden_layers
76
+ self.max_seq_len = max_seq_len
77
+ self.vocab_size = vocab_size
78
+ self.rms_norm_eps = rms_norm_eps
79
+ self.moe_intermediate_size = moe_intermediate_size
80
+ self.moe_num_experts = moe_num_experts
81
+ self.moe_top_k = moe_top_k
82
+ self.rope_theta = rope_theta
83
+ self.rope_scaling = rope_scaling
84
+ self.max_position_embedding = max_position_embedding
85
+ self.share_expert_dim = share_expert_dim
86
+ self.share_q_dim = share_q_dim
87
+ self.head_dim = head_dim
88
+ self.norm_expert_weight = norm_expert_weight
89
+ self.moe_layers_enum = moe_layers_enum
90
+
91
+ super().__init__(**kwargs)
92
+
93
+
94
+class Step3VLConfig(PretrainedConfig):
95
+ model_type = "step3_vl"
96
+
97
+ def __init__(
98
+ self,
99
+ vision_config: Optional[Union[dict, Step3VisionEncoderConfig]] = None,
100
+ text_config: Optional[Union[dict, Step3TextConfig]] = None,
101
+ understand_projector_stride: int = 1,
102
+ projector_bias: bool = True,
103
+ image_token_id: int = 128001,
104
+ **kwargs,
105
+ ) -> None:
106
+ if vision_config is None:
107
+ vision_config = Step3VisionEncoderConfig()
108
+ elif isinstance(vision_config, dict):
109
+ vision_config = Step3VisionEncoderConfig(**vision_config)
110
+ self.vision_config = vision_config
111
+
112
+ if text_config is None:
113
+ text_config = Step3TextConfig()
114
+ elif isinstance(text_config, dict):
115
+ text_config = Step3TextConfig(**text_config)
116
+ self.text_config = text_config
117
+
118
+ self.understand_projector_stride = understand_projector_stride
119
+ self.projector_bias = projector_bias
120
+ self.hidden_size = text_config.hidden_size
121
+ self.image_token_id = image_token_id
122
+
123
+ super().__init__(**kwargs)