@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 92effb07a4
Fork 0

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 12, 14:45 UTC

[Model] Add HunyuanOCR support (#29327)

Signed-off-by: manayang <jackmanayang@gmail.com> Signed-off-by: Isotr0py <mozf@mail2.sysu.edu.cn> Signed-off-by: Roger Wang <hey@rogerw.io> Co-authored-by: sergeywang <sergeywang@tencent.com> Co-authored-by: manayang <jackmanayang@gmail.com> Co-authored-by: manayang <manayang@tencent.com> Co-authored-by: Roger Wang <hey@rogerw.io>

Isotr0py committed Nov 25, 2025 at 11:28 UTC 92effb07a48e56c531a95b696acd5f699baf16da
18 files changed +2415 -4
docs/models/supported_models.md
+1
@@ -680,6 +680,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen
680 | `Glm4vMoeForConditionalGeneration` | GLM-4.5V | T + I<sup>E+</sup> + V<sup>E+</sup> | `zai-org/GLM-4.5V`, etc. | ✅︎ | ✅︎ |
681 | `GraniteSpeechForConditionalGeneration` | Granite Speech | T + A | `ibm-granite/granite-speech-3.3-8b` | ✅︎ | ✅︎ |
682 | `H2OVLChatModel` | H2OVL | T + I<sup>E+</sup> | `h2oai/h2ovl-mississippi-800m`, `h2oai/h2ovl-mississippi-2b`, etc. | | ✅︎ |
683 +| `HunYuanVLForConditionalGeneration` | HunyuanOCR | T + I<sup>E+</sup> | `tencent/HunyuanOCR`, etc. | ✅︎ | ✅︎ |
684 | `Idefics3ForConditionalGeneration` | Idefics3 | T + I | `HuggingFaceM4/Idefics3-8B-Llama3`, etc. | ✅︎ | |
685 | `InternS1ForConditionalGeneration` | Intern-S1 | T + I<sup>E+</sup> + V<sup>E+</sup> | `internlm/Intern-S1`, `internlm/Intern-S1-mini`, etc. | ✅︎ | ✅︎ |
686 | `InternVLChatModel` | InternVL 3.5, InternVL 3.0, InternVideo 2.5, InternVL 2.5, Mono-InternVL, InternVL 2.0 | T + I<sup>E+</sup> + (V<sup>E+</sup>) | `OpenGVLab/InternVL3_5-14B`, `OpenGVLab/InternVL3-9B`, `OpenGVLab/InternVideo2_5_Chat_8B`, `OpenGVLab/InternVL2_5-4B`, `OpenGVLab/Mono-InternVL-2B`, `OpenGVLab/InternVL2-4B`, etc. | ✅︎ | ✅︎ |
examples/offline_inference/vision_language.py
+26
@@ -538,6 +538,31 @@ def run_h2ovl(questions: list[str], modality: str) -> ModelRequestData:
538 )
539
540
541 +# HunyuanOCR
542 +def run_hunyuan_vl(questions: list[str], modality: str) -> ModelRequestData:
543 + assert modality == "image"
544 +
545 + model_name = "tencent/HunyuanOCR"
546 +
547 + engine_args = EngineArgs(
548 + model=model_name,
549 + max_model_len=8192,
550 + limit_mm_per_prompt={modality: 1},
551 + )
552 +
553 + placeholder = "<|hy_place▁holder▁no▁100|><|hy_place▁holder▁no▁102|><|hy_place▁holder▁no▁101|>" # noqa: E501
554 + prompts = [
555 + f"<|hy_begin▁of▁sentence|>{placeholder}{question}<|hy_User|>"
556 + for question in questions
557 + ]
558 +
559 + return ModelRequestData(
560 + engine_args=engine_args,
561 + prompts=prompts,
562 + stop_token_ids=None,
563 + )
564 +
565 +
566 # naver-hyperclovax/HyperCLOVAX-SEED-Vision-Instruct-3B
567 def run_hyperclovax_seed_vision(
568 questions: list[str], modality: str
@@ -1820,6 +1845,7 @@ model_example_map = {
1845 "glm4_5v": run_glm4_5v,
1846 "glm4_5v_fp8": run_glm4_5v_fp8,
1847 "h2ovl_chat": run_h2ovl,
1848 + "hunyuan_vl": run_hunyuan_vl,
1849 "hyperclovax_seed_vision": run_hyperclovax_seed_vision,
1850 "idefics3": run_idefics3,
1851 "interns1": run_interns1,
tests/models/registry.py
+4
@@ -626,6 +626,10 @@ _MULTIMODAL_EXAMPLE_MODELS = {
626 "naver-hyperclovax/HyperCLOVAX-SEED-Vision-Instruct-3B",
627 trust_remote_code=True,
628 ),
629 + "HunYuanVLForConditionalGeneration": _HfExamplesInfo(
630 + "tencent/HunyuanOCR",
631 + is_available_online=False,
632 + ),
633 "Idefics3ForConditionalGeneration": _HfExamplesInfo(
634 "HuggingFaceM4/Idefics3-8B-Llama3",
635 extras={"tiny": "HuggingFaceTB/SmolVLM-256M-Instruct"},
vllm/config/model.py
+5
@@ -33,6 +33,7 @@ from vllm.transformers_utils.config import (
33 try_get_safetensors_metadata,
34 try_get_tokenizer_config,
35 uses_mrope,
36 + uses_xdrope_dim,
37 )
38 from vllm.transformers_utils.gguf_utils import (
39 maybe_patch_hf_config_from_gguf,
@@ -1615,6 +1616,10 @@ class ModelConfig:
1616 def uses_mrope(self) -> bool:
1617 return uses_mrope(self.hf_config)
1618
1619 + @property
1620 + def uses_xdrope_dim(self) -> int:
1621 + return uses_xdrope_dim(self.hf_config)
1622 +
1623 @property
1624 def is_multimodal_model(self) -> bool:
1625 return self.multimodal_config is not None
vllm/model_executor/layers/rotary_embedding/__init__.py
+13
@@ -17,6 +17,7 @@ from .llama4_vision_rope import Llama4VisionRotaryEmbedding
17 from .mrope import MRotaryEmbedding
18 from .ntk_scaling_rope import NTKScalingRotaryEmbedding
19 from .phi3_long_rope_scaled_rope import Phi3LongRoPEScaledRotaryEmbedding
20 +from .xdrope import XDRotaryEmbedding
21 from .yarn_scaling_rope import YaRNScalingRotaryEmbedding
22
23 _ROPE_DICT: dict[tuple, RotaryEmbedding] = {}
@@ -184,6 +185,18 @@ def get_rope(
185 raise ValueError(
186 "Dynamic rope scaling must contain either 'alpha' or 'factor' field"
187 )
188 + elif scaling_type == "xdrope":
189 + scaling_alpha = rope_parameters["alpha"]
190 + rotary_emb = XDRotaryEmbedding(
191 + head_size,
192 + rotary_dim,
193 + max_position,
194 + base,
195 + is_neox_style,
196 + scaling_alpha,
197 + dtype,
198 + xdrope_section=rope_parameters["xdrope_section"],
199 + )
200 elif scaling_type == "yarn":
201 scaling_factor = rope_parameters["factor"]
202 original_max_position = rope_parameters["original_max_position_embeddings"]
vllm/model_executor/layers/rotary_embedding/xdrope.py new
+102
@@ -0,0 +1,102 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import numpy as np
5 +import torch
6 +
7 +from .common import apply_rotary_emb_dispatch
8 +from .dynamic_ntk_alpha_rope import DynamicNTKAlphaRotaryEmbedding
9 +
10 +
11 +class XDRotaryEmbedding(DynamicNTKAlphaRotaryEmbedding):
12 + """DynamicNTKAlphaRotaryEmbedding extended with MultiModal(XD) Sections.
13 +
14 + Based on the original DynamicNTKAlphaRotaryEmbedding implementation.
15 + """
16 +
17 + def __init__(
18 + self,
19 + head_size: int,
20 + rotary_dim: int,
21 + max_position_embeddings: int,
22 + base: float,
23 + is_neox_style: bool,
24 + scaling_alpha: float,
25 + dtype: torch.dtype,
26 + xdrope_section: list[int],
27 + ) -> None:
28 + self.xdrope_section = xdrope_section
29 + super().__init__(
30 + head_size,
31 + rotary_dim,
32 + max_position_embeddings,
33 + base,
34 + is_neox_style,
35 + scaling_alpha,
36 + dtype,
37 + )
38 +
39 + def forward(
40 + self,
41 + positions: torch.Tensor,
42 + query: torch.Tensor,
43 + key: torch.Tensor | None = None,
44 + offsets: torch.Tensor | None = None,
45 + ) -> tuple[torch.Tensor, torch.Tensor | None]:
46 + """PyTorch-native implementation equivalent to forward().
47 +
48 + Args:
49 + positions:
50 + [4, num_tokens] (P/W/H/T positions with multimodal inputs)
51 + query: [num_tokens, num_heads * head_size]
52 + key: [num_tokens, num_kv_heads * head_size]
53 + """
54 + assert positions.ndim == 2
55 + assert key is not None
56 +
57 + num_tokens = positions.shape[-1]
58 + cos_sin = self.cos_sin_cache[positions]
59 + cos, sin = cos_sin.chunk(2, dim=-1)
60 + cos = torch.cat(
61 + [m[i] for i, m in enumerate(cos.split(self.xdrope_section, dim=-1))], dim=-1
62 + )
63 + sin = torch.cat(
64 + [m[i] for i, m in enumerate(sin.split(self.xdrope_section, dim=-1))], dim=-1
65 + )
66 +
67 + query_shape = query.shape
68 + query = query.view(num_tokens, -1, self.head_size)
69 + query_rot = query[..., : self.rotary_dim]
70 + query_pass = query[..., self.rotary_dim :]
71 + query_rot = apply_rotary_emb_dispatch(query_rot, cos, sin, self.is_neox_style)
72 + query = torch.cat((query_rot, query_pass), dim=-1).reshape(query_shape)
73 +
74 + key_shape = key.shape
75 + key = key.view(num_tokens, -1, self.head_size)
76 + key_rot = key[..., : self.rotary_dim]
77 + key_pass = key[..., self.rotary_dim :]
78 + key_rot = apply_rotary_emb_dispatch(key_rot, cos, sin, self.is_neox_style)
79 + key = torch.cat((key_rot, key_pass), dim=-1).reshape(key_shape)
80 + return query, key
81 +
82 + @staticmethod
83 + def get_next_input_positions(
84 + context_len: int,
85 + seq_len: int,
86 + xd_sections: int = 4,
87 + ) -> list[list[int]]:
88 + return [list(range(context_len, seq_len)) for _ in range(xd_sections)]
89 +
90 + @staticmethod
91 + def get_next_input_positions_tensor(
92 + out: np.ndarray,
93 + out_offset: int,
94 + context_len: int,
95 + num_new_tokens: int,
96 + ):
97 + values = np.arange(
98 + context_len,
99 + context_len + num_new_tokens,
100 + dtype=out.dtype,
101 + )
102 + out[:, out_offset : out_offset + num_new_tokens] = values
vllm/model_executor/models/hunyuan_v1.py
+10 -1
@@ -576,7 +576,16 @@ class HunYuanDecoderLayer(nn.Module):
576 return hidden_states, residual, ori_kv_states
577
578
579 -@support_torch_compile
579 +@support_torch_compile(
580 + dynamic_arg_dims={
581 + "input_ids": 0,
582 + # positions is of shape (xd, seq_len) if xdrope is enabled for hunyuan-vl,
583 + # otherwise (seq_len, ).
584 + "positions": -1,
585 + "intermediate_tensors": 0,
586 + "inputs_embeds": 0,
587 + }
588 +)
589 class HunYuanModel(nn.Module):
590 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
591 super().__init__()
vllm/model_executor/models/hunyuan_vision.py new
+1028
@@ -0,0 +1,1028 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +# coding=utf-8
5 +# Copyright 2025 The HunYuan team.
6 +# Copyright 2025 The vLLM team.
7 +# Copyright 2025 EleutherAI and the HuggingFace Inc. team. All rights reserved.
8 +#
9 +# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
10 +# and OPT implementations in this library. It has been modified from its
11 +# original forms to accommodate minor architectural differences compared
12 +# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
13 +#
14 +# Licensed under the Apache License, Version 2.0 (the "License");
15 +# you may not use this file except in compliance with the License.
16 +# You may obtain a copy of the License at
17 +#
18 +# http://www.apache.org/licenses/LICENSE-2.0
19 +#
20 +# Unless required by applicable law or agreed to in writing, software
21 +# distributed under the License is distributed on an "AS IS" BASIS,
22 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23 +# See the License for the specific language governing permissions and
24 +# limitations under the License.
25 +"""Inference-only HunYuan-VL model compatible with HuggingFace weights."""
26 +
27 +from collections.abc import Callable, Iterable, Mapping, Sequence
28 +from functools import partial
29 +from typing import Annotated, Any, Literal, TypeAlias
30 +
31 +import torch
32 +import torch.nn as nn
33 +import torch.nn.functional as F
34 +from transformers import BatchFeature
35 +
36 +from vllm.attention.backends.registry import AttentionBackendEnum
37 +from vllm.attention.layer import MultiHeadAttention
38 +from vllm.config import MultiModalConfig, VllmConfig
39 +from vllm.config.multimodal import BaseDummyOptions
40 +from vllm.distributed import parallel_state
41 +from vllm.distributed import utils as dist_utils
42 +from vllm.logger import init_logger
43 +from vllm.model_executor.layers.activation import get_act_fn
44 +from vllm.model_executor.layers.layernorm import RMSNorm
45 +from vllm.model_executor.layers.linear import (
46 + ColumnParallelLinear,
47 + QKVParallelLinear,
48 + RowParallelLinear,
49 +)
50 +from vllm.model_executor.layers.quantization import QuantizationConfig
51 +from vllm.model_executor.model_loader.weight_utils import default_weight_loader
52 +from vllm.model_executor.models.module_mapping import MultiModelKeys
53 +from vllm.multimodal import MULTIMODAL_REGISTRY
54 +from vllm.multimodal.inputs import (
55 + ImageItem,
56 + ModalityData,
57 + MultiModalDataDict,
58 + MultiModalFeatureSpec,
59 + MultiModalFieldConfig,
60 + MultiModalKwargsItems,
61 +)
62 +from vllm.multimodal.parse import (
63 + DictEmbeddingItems,
64 + ImageSize,
65 + MultiModalDataItems,
66 + MultiModalDataParser,
67 +)
68 +from vllm.multimodal.processing import (
69 + BaseMultiModalProcessor,
70 + BaseProcessingInfo,
71 + PromptReplacement,
72 + PromptUpdate,
73 +)
74 +from vllm.multimodal.profiling import BaseDummyInputsBuilder
75 +from vllm.sequence import IntermediateTensors
76 +from vllm.transformers_utils.configs.hunyuan_vl import (
77 + HunYuanVLConfig,
78 + HunYuanVLVisionConfig,
79 +)
80 +from vllm.transformers_utils.processors.hunyuan_vl import HunYuanVLProcessor
81 +from vllm.transformers_utils.processors.hunyuan_vl_image import smart_resize
82 +from vllm.utils.tensor_schema import TensorSchema, TensorShape
83 +
84 +from .interfaces import (
85 + MultiModalEmbeddings,
86 + SupportsLoRA,
87 + SupportsMultiModal,
88 + SupportsPP,
89 + SupportsQuant,
90 + SupportsXDRoPE,
91 +)
92 +from .utils import (
93 + AutoWeightsLoader,
94 + WeightsMapper,
95 + init_vllm_registered_model,
96 + maybe_prefix,
97 +)
98 +
99 +logger = init_logger(__name__)
100 +
101 +# === Vision Inputs === #
102 +
103 +
104 +class HunYuanVLImagePixelInputs(TensorSchema):
105 + """
106 + Dimensions:
107 + - np: Number of patches
108 + - ni: Number of images
109 + - cps: Number of channels * patch_size * patch_size
110 + """
111 +
112 + type: Literal["pixel_values"]
113 +
114 + pixel_values: Annotated[
115 + torch.Tensor,
116 + TensorShape("np", "cps"),
117 + ]
118 +
119 + image_grid_thw: Annotated[
120 + torch.Tensor,
121 + TensorShape("ni", 3),
122 + ]
123 +
124 +
125 +class HunYuanVLImageEmbeddingInputs(TensorSchema):
126 + """
127 + Dimensions:
128 + - nf: Number of image features
129 + - hs: Hidden size
130 + - ni: Number of images
131 + """
132 +
133 + type: Literal["image_embeds"]
134 +
135 + image_embeds: Annotated[
136 + torch.Tensor,
137 + TensorShape("nf", "hs"),
138 + ]
139 +
140 + image_grid_thw: Annotated[
141 + torch.Tensor,
142 + TensorShape("ni", 3),
143 + ]
144 +
145 +
146 +HunYuanVLImageInputs: TypeAlias = (
147 + HunYuanVLImagePixelInputs | HunYuanVLImageEmbeddingInputs
148 +)
149 +
150 +# === Vision Encoder === #
151 +
152 +
153 +class HunYuanVisionMLP(nn.Module):
154 + def __init__(
155 + self,
156 + in_features: int,
157 + hidden_features: int,
158 + bias: bool = True,
159 + act_fn: Callable[[torch.Tensor], torch.Tensor] = F.gelu,
160 + quant_config: QuantizationConfig | None = None,
161 + prefix: str = "",
162 + use_data_parallel: bool = False,
163 + ):
164 + super().__init__()
165 + self.dense_h_to_4h = ColumnParallelLinear(
166 + in_features,
167 + hidden_features,
168 + bias=bias,
169 + quant_config=quant_config,
170 + prefix=f"{prefix}.dense_h_to_4h",
171 + disable_tp=use_data_parallel,
172 + )
173 + self.dense_4h_to_h = RowParallelLinear(
174 + hidden_features,
175 + in_features,
176 + bias=bias,
177 + quant_config=quant_config,
178 + prefix=f"{prefix}.dense_4h_to_h",
179 + disable_tp=use_data_parallel,
180 + )
181 + self.act_fn = act_fn
182 +
183 + def forward(self, x: torch.Tensor):
184 + x_up, _ = self.dense_h_to_4h(x)
185 + x_down, _ = self.dense_4h_to_h(self.act_fn(x_up))
186 + return x_down
187 +
188 +
189 +class HunYuanVisionAttention(nn.Module):
190 + def __init__(
191 + self,
192 + embed_dim: int,
193 + num_heads: int,
194 + projection_size: int,
195 + quant_config: QuantizationConfig | None = None,
196 + multimodal_config: MultiModalConfig | None = None,
197 + prefix: str = "",
198 + use_data_parallel: bool = False,
199 + ) -> None:
200 + super().__init__()
201 + # Per attention head and per partition values.
202 + self.tp_size = (
203 + 1
204 + if use_data_parallel
205 + else parallel_state.get_tensor_model_parallel_world_size()
206 + )
207 + self.hidden_size_per_attention_head = dist_utils.divide(
208 + projection_size, num_heads
209 + )
210 + self.num_attention_heads_per_partition = dist_utils.divide(
211 + num_heads, self.tp_size
212 + )
213 +
214 + self.qkv = QKVParallelLinear(
215 + hidden_size=embed_dim,
216 + head_size=self.hidden_size_per_attention_head,
217 + total_num_heads=num_heads,
218 + total_num_kv_heads=num_heads,
219 + bias=True,
220 + quant_config=quant_config,
221 + prefix=f"{prefix}.qkv",
222 + disable_tp=use_data_parallel,
223 + )
224 +
225 + self.o_proj = RowParallelLinear(
226 + input_size=projection_size,
227 + output_size=embed_dim,
228 + quant_config=quant_config,
229 + prefix=f"{prefix}.o_proj",
230 + disable_tp=use_data_parallel,
231 + )
232 +
233 + self.scale = self.hidden_size_per_attention_head**-0.5
234 + self.attn = MultiHeadAttention(
235 + self.num_attention_heads_per_partition,
236 + self.hidden_size_per_attention_head,
237 + self.scale,
238 + prefix=f"{prefix}.attn",
239 + multimodal_config=multimodal_config,
240 + )
241 +
242 + def forward(
243 + self,
244 + x: torch.Tensor,
245 + ) -> torch.Tensor:
246 + qkv, _ = self.qkv(x)
247 + q, k, v = qkv.chunk(3, dim=-1)
248 + out = self.attn(q, k, v)
249 + output, _ = self.o_proj(out)
250 + return output
251 +
252 +
253 +class HunYuanVisionBlock(nn.Module):
254 + def __init__(
255 + self,
256 + dim: int,
257 + num_heads: int,
258 + mlp_hidden_dim: int,
259 + act_fn: Callable[[torch.Tensor], torch.Tensor] = F.gelu,
260 + norm_layer: Callable[[int], nn.Module] | None = None,
261 + quant_config: QuantizationConfig | None = None,
262 + multimodal_config: MultiModalConfig | None = None,
263 + prefix: str = "",
264 + use_data_parallel: bool = False,
265 + ) -> None:
266 + super().__init__()
267 + if norm_layer is None:
268 + norm_layer = partial(nn.LayerNorm, eps=1e-6)
269 + self.input_layernorm = norm_layer(dim)
270 + self.post_attention_layernorm = norm_layer(dim)
271 + self.self_attn = HunYuanVisionAttention(
272 + embed_dim=dim,
273 + num_heads=num_heads,
274 + projection_size=dim,
275 + quant_config=quant_config,
276 + multimodal_config=multimodal_config,
277 + prefix=f"{prefix}.self_attn",
278 + use_data_parallel=use_data_parallel,
279 + )
280 + self.mlp = HunYuanVisionMLP(
281 + dim,
282 + mlp_hidden_dim,
283 + act_fn=act_fn,
284 + bias=True,
285 + quant_config=quant_config,
286 + prefix=f"{prefix}.mlp",
287 + use_data_parallel=use_data_parallel,
288 + )
289 +
290 + def forward(
291 + self,
292 + x: torch.Tensor,
293 + ) -> torch.Tensor:
294 + x = x + self.self_attn(self.input_layernorm(x))
295 + x = x + self.mlp(self.post_attention_layernorm(x))
296 + return x
297 +
298 +
299 +class HunYuanVisionPatchEmbed(nn.Module):
300 + def __init__(self, config: HunYuanVLVisionConfig):
301 + super().__init__()
302 +
303 + self.config = config
304 + self.embed_dim = config.hidden_size
305 + self.patch_size = config.patch_size
306 + self.num_channels = config.num_channels
307 + self.spatial_merge_size = config.spatial_merge_size
308 + self.interpolate_mode = config.interpolate_mode
309 +
310 + self.patch_embedding = nn.Conv2d(
311 + in_channels=config.num_channels,
312 + out_channels=self.embed_dim,
313 + kernel_size=self.patch_size,
314 + stride=self.patch_size,
315 + bias=True,
316 + )
317 +
318 + self.max_num_patches = (config.max_image_size // self.patch_size) ** 2
319 +
320 + self.num_positions = self.max_num_patches + 1
321 + self.position_edge = int(self.num_positions**0.5)
322 + # first token is cls token, skip it
323 + self.position_embedding = nn.Embedding(self.num_positions, self.embed_dim)
324 +
325 + self.patch_pos_embed = None
326 +
327 + def forward(
328 + self, pixel_values: torch.Tensor, grid_thw: list[list[int]]
329 + ) -> torch.Tensor:
330 + num_patches = pixel_values.size(0)
331 + pixel_values = pixel_values.reshape(
332 + num_patches, self.num_channels, self.patch_size, self.patch_size
333 + )
334 +
335 + patch_embeds = self.patch_embedding(pixel_values)
336 + patch_embeds = patch_embeds.squeeze(-1).squeeze(-1).unsqueeze(0)
337 +
338 + if self.patch_pos_embed is None:
339 + patch_pos_shape = (
340 + 1,
341 + self.position_edge,
342 + self.position_edge,
343 + self.embed_dim,
344 + )
345 + self.patch_pos_embed = (
346 + self.position_embedding.weight[1:, :]
347 + .reshape(patch_pos_shape)
348 + .permute(0, 3, 1, 2)
349 + .float()
350 + )
351 +
352 + patch_pos_embed_list = []
353 + for grid in grid_thw:
354 + _, h0, w0 = grid
355 + # we add a small number to avoid floating point error in the interpolation
356 + # see discussion at https://github.com/facebookresearch/dino/issues/8
357 + h0, w0 = h0 + 0.1, w0 + 0.1
358 + patch_pos_embed = nn.functional.interpolate(
359 + self.patch_pos_embed,
360 + scale_factor=(h0 / self.position_edge, w0 / self.position_edge),
361 + mode=self.interpolate_mode,
362 + align_corners=False,
363 + )
364 +
365 + patch_pos_embed = (
366 + patch_pos_embed.reshape(self.embed_dim, -1)
367 + .transpose(0, 1)
368 + .unsqueeze(0)
369 + .to(patch_embeds.dtype)
370 + )
371 + patch_pos_embed_list.append(patch_pos_embed)
372 +
373 + patch_pos_embed = torch.cat(patch_pos_embed_list, dim=1)
374 + embeddings = patch_embeds + patch_pos_embed
375 +
376 + return embeddings
377 +
378 +
379 +class HunYuanVisionPatchMerger(nn.Module):
380 + def __init__(
381 + self,
382 + in_channels,
383 + out_channels,
384 + spatial_merge_size=2,
385 + rms_norm_eps=1e-5,
386 + prefix="",
387 + ):
388 + super().__init__()
389 + self.spatial_merge_size = spatial_merge_size
390 + embed_std = out_channels**-0.5
391 +
392 + self.proj = nn.Sequential(
393 + nn.Conv2d(
394 + in_channels,
395 + in_channels * 2,
396 + kernel_size=spatial_merge_size,
397 + stride=spatial_merge_size,
398 + ),
399 + nn.GELU(),
400 + nn.Conv2d(in_channels * 2, in_channels * 4, kernel_size=1),
401 + )
402 + self.mlp = nn.Linear(in_channels * 4, out_channels)
403 +
404 + self.image_newline = nn.Parameter(torch.randn(in_channels * 4) * embed_std)
405 + self.image_begin = nn.Parameter(torch.randn(out_channels) * embed_std)
406 + self.image_end = nn.Parameter(torch.randn(out_channels) * embed_std)
407 + self.image_sep = nn.Parameter(torch.randn(out_channels) * embed_std)
408 +
409 + self.before_rms = RMSNorm(in_channels, eps=rms_norm_eps)
410 + self.after_rms = RMSNorm(out_channels, eps=rms_norm_eps)
411 +
412 + def forward(self, x, size=(16, 16)):
413 + x = self.before_rms(x)
414 +
415 + h, w = size
416 + dtype = x.dtype
417 + x = x.permute(0, 2, 1).reshape(x.shape[0], -1, h, w)
418 +
419 + x = self.proj(x) # b,c,h,w
420 + b, c, h, w = x.shape
421 + x = torch.cat(
422 + [x, self.image_newline.reshape(1, c, 1, 1).expand(b, c, h, 1).to(dtype)],
423 + dim=-1,
424 + )
425 + x = x.reshape(b, c, -1).permute(0, 2, 1)
426 + x = self.mlp(x)
427 +
428 + begin = self.image_begin.reshape(1, 1, -1).expand(b, 1, x.shape[-1]).to(dtype)
429 + end = self.image_end.reshape(1, 1, -1).expand(b, 1, x.shape[-1]).to(dtype)
430 + x = torch.cat([begin, x, end], dim=1)
431 +
432 + return self.after_rms(x)
433 +
434 +
435 +class HunYuanVisionTransformer(nn.Module):
436 + def __init__(
437 + self,
438 + vision_config: HunYuanVLVisionConfig,
439 + quant_config: QuantizationConfig | None = None,
440 + prefix: str = "",
441 + use_data_parallel: bool = False,
442 + multimodal_config: MultiModalConfig | None = None,
443 + attn_backend_override: AttentionBackendEnum | None = None,
444 + ) -> None:
445 + super().__init__()
446 +
447 + num_hidden_layers = vision_config.num_hidden_layers
448 + self.hidden_size = vision_config.hidden_size
449 + self.num_heads = vision_config.num_attention_heads
450 + self.spatial_merge_size = vision_config.spatial_merge_size
451 +
452 + from vllm.compilation.backends import set_model_tag
453 +
454 + with set_model_tag("HunYuanVisionPatchEmbed"):
455 + self.embeddings = HunYuanVisionPatchEmbed(vision_config)
456 +
457 + norm_layer = partial(nn.LayerNorm, eps=vision_config.rms_norm_eps)
458 +
459 + with set_model_tag("HunYuanVisionBlock"):
460 + self.layers = nn.ModuleList(
461 + [
462 + HunYuanVisionBlock(
463 + dim=vision_config.hidden_size,
464 + num_heads=vision_config.num_attention_heads,
465 + mlp_hidden_dim=vision_config.intermediate_size,
466 + act_fn=get_act_fn(vision_config.hidden_act),
467 + norm_layer=norm_layer,
468 + quant_config=quant_config,
469 + multimodal_config=multimodal_config,
470 + prefix=f"{prefix}.layers.{layer_idx}",
471 + use_data_parallel=use_data_parallel,
472 + )
473 + for layer_idx in range(num_hidden_layers)
474 + ]
475 + )
476 +
477 + with set_model_tag("HunYuanVisionPatchMerger"):
478 + self.perceive = HunYuanVisionPatchMerger(
479 + vision_config.hidden_size,
480 + vision_config.out_hidden_size,
481 + spatial_merge_size=vision_config.spatial_merge_size,
482 + rms_norm_eps=vision_config.rms_norm_eps,
483 + prefix=f"{prefix}.perceive",
484 + )
485 +
486 + @property
487 + def dtype(self) -> torch.dtype:
488 + return self.embeddings.patch_embedding.weight.dtype
489 +
490 + @property
491 + def device(self) -> torch.device:
492 + return self.embeddings.patch_embedding.weight.device
493 +
494 + def forward(
495 + self,
496 + x: torch.Tensor,
497 + grid_thw: list[list[int]],
498 + ) -> torch.Tensor:
499 + # patchify
500 + seq_len = x.size(0)
501 + cu_seqlens: list = [0]
502 +
503 + hidden_states = x.to(device=self.device, dtype=self.dtype)
504 + hidden_states = self.embeddings(hidden_states, grid_thw)
505 +
506 + for t, h, w in grid_thw:
507 + t, h, w = int(t), int(h), int(w)
508 + cu_seqlens.append(h * w)
509 +
510 + cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int32)
511 + cu_seqlens = torch.cumsum(cu_seqlens, dim=0, dtype=torch.int32)
512 +
513 + cu_seqlens = cu_seqlens.to(device=self.device, non_blocking=True)
514 +
515 + hidden_states = hidden_states.reshape(seq_len, -1)
516 + hidden_states = hidden_states.unsqueeze(0)
517 + for layer_num, layer in enumerate(self.layers):
518 + hidden_states = layer(hidden_states)
519 +
520 + # adapter
521 + split_lengths = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
522 + split_items = hidden_states.split(split_lengths, dim=1)
523 + image_embeds_list = []
524 + for grid, split_item in zip(grid_thw, split_items):
525 + image_embeds_list.append(
526 + self.perceive(split_item.contiguous(), size=grid[1:]).squeeze(0)
527 + )
528 +
529 + return image_embeds_list
530 +
531 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
532 + stacked_params_mapping = [
533 + # (param_name, shard_name, shard_id)
534 + (".qkv", ".q_proj", "q"),
535 + (".qkv", ".k_proj", "k"),
536 + (".qkv", ".v_proj", "v"),
537 + ]
538 + params_dict = dict(self.named_parameters(remove_duplicate=False))
539 + loaded_params: set[str] = set()
540 +
541 + for name, loaded_weight in weights:
542 + for param_name, weight_name, shard_id in stacked_params_mapping:
543 + if weight_name not in name:
544 + continue
545 + name = name.replace(weight_name, param_name)
546 +
547 + param = params_dict[name]
548 + weight_loader = param.weight_loader
549 + weight_loader(param, loaded_weight, shard_id)
550 + break
551 + else:
552 + param = params_dict[name]
553 + weight_loader = getattr(param, "weight_loader", default_weight_loader)
554 + weight_loader(param, loaded_weight)
555 + loaded_params.add(name)
556 + return loaded_params
557 +
558 +
559 +def _hunyuan_vl_field_config(hf_inputs: Mapping[str, torch.Tensor]):
560 + image_grid_thw = hf_inputs.get("image_grid_thw", torch.empty((0, 3)))
561 + image_grid_sizes = image_grid_thw.prod(-1)
562 + return dict(
563 + pixel_values=MultiModalFieldConfig.flat_from_sizes("image", image_grid_sizes),
564 + image_embeds=MultiModalFieldConfig.flat_from_sizes("image", image_grid_sizes),
565 + image_grid_thw=MultiModalFieldConfig.batched("image"),
566 + )
567 +
568 +
569 +class HunYuanVLMultiModalDataParser(MultiModalDataParser):
570 + def _parse_image_data(
571 + self,
572 + data: dict[str, torch.Tensor] | ModalityData[ImageItem],
573 + ):
574 + if isinstance(data, dict):
575 + return DictEmbeddingItems(
576 + data,
577 + modality="image",
578 + required_fields={"image_embeds", "image_grid_thw"},
579 + fields_factory=_hunyuan_vl_field_config,
580 + )
581 +
582 + return super()._parse_image_data(data)
583 +
584 +
585 +class HunYuanVLProcessingInfo(BaseProcessingInfo):
586 + def get_hf_config(self):
587 + return self.ctx.get_hf_config(HunYuanVLConfig)
588 +
589 + def get_hf_processor(
590 + self,
591 + **kwargs: object,
592 + ) -> HunYuanVLProcessor:
593 + return self.ctx.get_hf_processor(
594 + HunYuanVLProcessor,
595 + use_fast=kwargs.pop("use_fast", True),
596 + **kwargs,
597 + )
598 +
599 + def get_image_processor(
600 + self,
601 + **kwargs: object,
602 + ) -> HunYuanVLProcessor:
603 + return self.get_hf_processor(**kwargs).image_processor
604 +
605 + def get_supported_mm_limits(self) -> Mapping[str, int | None]:
606 + return {"image": None}
607 +
608 + def get_mm_max_tokens_per_item(
609 + self,
610 + seq_len: int,
611 + mm_counts: Mapping[str, int],
612 + ) -> Mapping[str, int]:
613 + max_image_tokens = self.get_max_image_tokens()
614 + # TODO: support video
615 + max_video_tokens = 0
616 + return {"image": max_image_tokens, "video": max_video_tokens}
617 +
618 + def _get_vision_info(
619 + self,
620 + *,
621 + image_width: int,
622 + image_height: int,
623 + num_frames: int = 1,
624 + do_resize: bool = True,
625 + image_processor: HunYuanVLProcessor | None,
626 + ) -> tuple[ImageSize, int]:
627 + if image_processor is None:
628 + image_processor = self.get_image_processor()
629 +
630 + hf_config = self.get_hf_config()
631 + vision_config = hf_config.vision_config
632 + patch_size = vision_config.patch_size
633 + spatial_merge_size = vision_config.spatial_merge_size
634 +
635 + if do_resize:
636 + resized_height, resized_width = smart_resize(
637 + height=image_height,
638 + width=image_width,
639 + factor=patch_size * spatial_merge_size,
640 + min_pixels=image_processor.min_pixels,
641 + max_pixels=image_processor.max_pixels,
642 + )
643 + preprocessed_size = ImageSize(width=resized_width, height=resized_height)
644 + else:
645 + preprocessed_size = ImageSize(width=image_width, height=image_height)
646 +
647 + grid_t = 1
648 + grid_h = preprocessed_size.height // patch_size
649 + grid_w = preprocessed_size.width // patch_size
650 +
651 + num_vision_tokens = (
652 + grid_t * grid_h // spatial_merge_size * (grid_w // spatial_merge_size + 1)
653 + + 2
654 + )
655 +
656 + return preprocessed_size, num_vision_tokens
657 +
658 + def get_num_image_tokens(
659 + self,
660 + *,
661 + image_width: int,
662 + image_height: int,
663 + image_processor: HunYuanVLProcessor | None,
664 + ) -> int:
665 + _, num_image_tokens = self._get_vision_info(
666 + image_width=image_width,
667 + image_height=image_height,
668 + image_processor=image_processor,
669 + )
670 + return num_image_tokens
671 +
672 + def get_image_size_with_most_features(self) -> ImageSize:
673 + max_image_size, _ = self._get_vision_info(
674 + image_width=512,
675 + image_height=8192,
676 + image_processor=None,
677 + )
678 + return max_image_size
679 +
680 + def get_max_image_tokens(self) -> int:
681 + target_width, target_height = self.get_image_size_with_most_features()
682 + return self.get_num_image_tokens(
683 + image_width=target_width,
684 + image_height=target_height,
685 + image_processor=None,
686 + )
687 +
688 +
689 +class HunYuanVLDummyInputsBuilder(BaseDummyInputsBuilder[HunYuanVLProcessingInfo]):
690 + def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
691 + num_images = mm_counts.get("image", 0)
692 +
693 + hf_processor = self.info.get_hf_processor()
694 + image_token: str = hf_processor.image_token
695 +
696 + return image_token * num_images
697 +
698 + def get_dummy_mm_data(
699 + self,
700 + seq_len: int,
701 + mm_counts: Mapping[str, int],
702 + mm_options: Mapping[str, BaseDummyOptions] | None = None,
703 + ) -> MultiModalDataDict:
704 + num_images = mm_counts.get("image", 1)
705 +
706 + target_width, target_height = self.info.get_image_size_with_most_features()
707 +
708 + return {
709 + "image": self._get_dummy_images(
710 + width=target_width, height=target_height, num_images=num_images
711 + ),
712 + }
713 +
714 +
715 +class HunYuanVLMultiModalProcessor(BaseMultiModalProcessor[HunYuanVLProcessingInfo]):
716 + def _get_data_parser(self) -> MultiModalDataParser:
717 + return HunYuanVLMultiModalDataParser()
718 +
719 + def _call_hf_processor(
720 + self,
721 + prompt: str,
722 + mm_data: Mapping[str, object],
723 + mm_kwargs: Mapping[str, object],
724 + tok_kwargs: Mapping[str, object],
725 + ) -> BatchFeature:
726 + return self.info.ctx.call_hf_processor(
727 + self.info.get_hf_processor(**mm_kwargs),
728 + dict(text=prompt, **mm_data),
729 + dict(**mm_kwargs, **tok_kwargs),
730 + )
731 +
732 + def _get_prompt_updates(
733 + self,
734 + mm_items: MultiModalDataItems,
735 + hf_processor_mm_kwargs: Mapping[str, Any],
736 + out_mm_kwargs: MultiModalKwargsItems,
737 + ) -> Sequence[PromptUpdate]:
738 + hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
739 + image_processor = self.info.get_image_processor(**hf_processor_mm_kwargs)
740 +
741 + placeholder = {
742 + "image": hf_processor.image_token_id,
743 + }
744 +
745 + merge_size = image_processor.merge_size
746 +
747 + def get_replacement_hunyuan_vl(item_idx: int, modality: str):
748 + out_item = out_mm_kwargs[modality][item_idx]
749 + grid_thw = out_item[f"{modality}_grid_thw"].data
750 + assert isinstance(grid_thw, torch.Tensor)
751 +
752 + _, grid_h, grid_w = grid_thw
753 + num_tokens = (int(grid_h) // merge_size) * (
754 + int(grid_w) // merge_size + 1
755 + ) + 2
756 + return [placeholder[modality]] * num_tokens
757 +
758 + return [
759 + PromptReplacement(
760 + modality=modality,
761 + target=[placeholder[modality]],
762 + replacement=partial(get_replacement_hunyuan_vl, modality=modality),
763 + )
764 + for modality in ("image",)
765 + ]
766 +
767 + def _get_mm_fields_config(
768 + self,
769 + hf_inputs: BatchFeature,
770 + hf_processor_mm_kwargs: Mapping[str, object],
771 + ) -> Mapping[str, MultiModalFieldConfig]:
772 + return _hunyuan_vl_field_config(hf_inputs)
773 +
774 +
775 +@MULTIMODAL_REGISTRY.register_processor(
776 + HunYuanVLMultiModalProcessor,
777 + info=HunYuanVLProcessingInfo,
778 + dummy_inputs=HunYuanVLDummyInputsBuilder,
779 +)
780 +class HunYuanVLForConditionalGeneration(
781 + nn.Module,
782 + SupportsMultiModal,
783 + SupportsLoRA,
784 + SupportsPP,
785 + SupportsQuant,
786 + SupportsXDRoPE,
787 +):
788 + multimodal_cpu_fields = {"image_grid_thw"}
789 +
790 + # To ensure correct weight loading and mapping.
791 + hf_to_vllm_mapper = WeightsMapper(
792 + orig_to_new_prefix={
793 + # mapping for new names in checkpoint saved after transformers v4.52
794 + "vit.vit.": "visual.",
795 + "vit.": "visual.",
796 + "model.": "language_model.model.",
797 + }
798 + )
799 +
800 + supports_encoder_tp_data = True
801 +
802 + def get_xdrope_input_positions(
803 + self,
804 + input_tokens: list[int],
805 + mm_features: list[MultiModalFeatureSpec],
806 + ) -> torch.Tensor:
807 + kwargs = MultiModalFeatureSpec.gather_kwargs(
808 + mm_features,
809 + {"image_grid_thw"},
810 + )
811 + image_grid_thw = [item.tolist() for item in kwargs.get("image_grid_thw", [])]
812 +
813 + hf_config = self.config
814 + image_start_token_id = hf_config.image_start_token_id
815 + spatial_merge_size = hf_config.vision_config.spatial_merge_size
816 + xd_num = len(hf_config.rope_scaling["xdrope_section"])
817 +
818 + input_tokens_tensor = torch.tensor(input_tokens)
819 + image_start_indices = torch.argwhere(
820 + input_tokens_tensor == image_start_token_id
821 + ).squeeze(1)
822 +
823 + p_index = torch.arange(len(input_tokens_tensor))
824 + w_index = torch.arange(len(input_tokens_tensor))
825 + h_index = torch.arange(len(input_tokens_tensor))
826 + t_index = torch.arange(len(input_tokens_tensor))
827 + for image_index in range(len(image_start_indices)):
828 + # +1 : first image_token, +2: for xdrope positions
829 + pos = image_start_indices[image_index] + 2
830 + t, h, w = image_grid_thw[image_index]
831 + _, llm_grid_h, llm_grid_w = (
832 + t,
833 + h // spatial_merge_size,
834 + w // spatial_merge_size,
835 + )
836 +
837 + token_num = (llm_grid_w + 1) * llm_grid_h
838 + w_index[pos : pos + token_num].copy_(
839 + torch.arange(0, llm_grid_w + 1)
840 + .reshape(1, -1)
841 + .expand(llm_grid_h, -1)
842 + .reshape(-1)
843 + )
844 + h_index[pos : pos + token_num].copy_(
845 + torch.arange(0, llm_grid_h)
846 + .reshape(-1, 1)
847 + .expand(-1, llm_grid_w + 1)
848 + .reshape(-1)
849 + )
850 + h_index[pos : pos + token_num] = 0
851 +
852 + if xd_num == 4:
853 + llm_positions = torch.stack([p_index, w_index, h_index, t_index])
854 + elif xd_num == 3:
855 + llm_positions = torch.stack([w_index, h_index, t_index])
856 +
857 + return llm_positions
858 +
859 + @classmethod
860 + def get_placeholder_str(cls, modality: str, i: int) -> str | None:
861 + if modality.startswith("image"):
862 + return "<|hy_place▁holder▁no▁100|><|hy_place▁holder▁no▁102|><|hy_place▁holder▁no▁101|>" # noqa: E501
863 +
864 + raise ValueError("Only image modality is supported")
865 +
866 + def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
867 + super().__init__()
868 + config: HunYuanVLConfig = vllm_config.model_config.hf_config
869 + multimodal_config = vllm_config.model_config.multimodal_config
870 +
871 + self.config = config
872 + self.multimodal_config = multimodal_config
873 +
874 + if multimodal_config.get_limit_per_prompt("image"):
875 + attn_backend_override = (
876 + multimodal_config.mm_encoder_attn_backend
877 + if multimodal_config is not None
878 + else None
879 + )
880 + self.visual = HunYuanVisionTransformer(
881 + config.vision_config,
882 + quant_config=self.quant_config,
883 + prefix=maybe_prefix(prefix, "visual"),
884 + multimodal_config=multimodal_config,
885 + attn_backend_override=attn_backend_override,
886 + )
887 + else:
888 + self.visual = None
889 +
890 + self.language_model = init_vllm_registered_model(
891 + vllm_config=vllm_config,
892 + prefix=maybe_prefix(prefix, "language_model.model"),
893 + architectures=[
894 + "HunYuanDenseV1ForCausalLM",
895 + "HunYuanMoEV1ForCausalLM",
896 + ],
897 + )
898 +
899 + self.make_empty_intermediate_tensors = (
900 + self.language_model.make_empty_intermediate_tensors
901 + )
902 +
903 + def _parse_and_validate_image_input(
904 + self, **kwargs: object
905 + ) -> HunYuanVLImageInputs | None:
906 + pixel_values = kwargs.pop("pixel_values", None)
907 + image_embeds = kwargs.pop("image_embeds", None)
908 + image_grid_thw = kwargs.pop("image_grid_thw", None)
909 +
910 + if pixel_values is None and image_embeds is None:
911 + return None
912 +
913 + # TODO: refine
914 + if isinstance(pixel_values, list):
915 + pixel_values = torch.cat(pixel_values, dim=0)
916 + if len(pixel_values.shape) == 3:
917 + last_dim = pixel_values.shape[-1]
918 + pixel_values = pixel_values.reshape(-1, last_dim)
919 + image_grid_thw = image_grid_thw.reshape(-1, 3)
920 +
921 + if pixel_values is not None:
922 + return HunYuanVLImagePixelInputs(
923 + type="pixel_values",
924 + pixel_values=pixel_values,
925 + image_grid_thw=image_grid_thw,
926 + )
927 +
928 + if image_embeds is not None:
929 + return HunYuanVLImageEmbeddingInputs(
930 + type="image_embeds",
931 + image_embeds=image_embeds,
932 + image_grid_thw=image_grid_thw,
933 + )
934 +
935 + def _process_image_input(
936 + self, image_input: HunYuanVLImageInputs
937 + ) -> tuple[torch.Tensor, ...]:
938 + grid_thw = image_input["image_grid_thw"]
939 + assert grid_thw.ndim == 2
940 + grid_thw_list = grid_thw.tolist()
941 +
942 + if image_input["type"] == "image_embeds":
943 + image_embeds = image_input["image_embeds"].type(self.visual.dtype)
944 + else:
945 + pixel_values = image_input["pixel_values"]
946 +
947 + # TODO: use_data_parallel (split image_embeds in visual)
948 + image_embeds = self.visual(pixel_values, grid_thw=grid_thw_list)
949 +
950 + return image_embeds
951 +
952 + def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
953 + mm_input_by_modality = {}
954 +
955 + # Preserve the order of modalities if there are multiple of them
956 + # from the order of kwargs.
957 + for input_key in kwargs:
958 + if (
959 + input_key in ("pixel_values", "image_embeds")
960 + and "image" not in mm_input_by_modality
961 + ):
962 + mm_input_by_modality["image"] = self._parse_and_validate_image_input(
963 + **kwargs
964 + )
965 + return mm_input_by_modality
966 +
967 + def get_language_model(self) -> torch.nn.Module:
968 + return self.language_model
969 +
970 + def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
971 + mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
972 + if not mm_input_by_modality:
973 + return []
974 +
975 + # The result multimodal_embeddings is tuple of tensors, with each
976 + # tensor correspoending to a multimodal data item (image or video).
977 + multimodal_embeddings: tuple[torch.Tensor, ...] = ()
978 +
979 + # NOTE: It is important to iterate over the keys in this dictionary
980 + # to preserve the order of the modalities.
981 + for modality in mm_input_by_modality:
982 + multimodal_input = mm_input_by_modality[modality]
983 + if modality == "image":
984 + image_embeddings = self._process_image_input(multimodal_input)
985 + multimodal_embeddings += tuple(image_embeddings)
986 + return multimodal_embeddings
987 +
988 + def forward(
989 + self,
990 + input_ids: torch.Tensor,
991 + positions: torch.Tensor,
992 + intermediate_tensors: IntermediateTensors | None,
993 + inputs_embeds: torch.Tensor | None,
994 + **kwargs: object,
995 + ) -> torch.Tensor | IntermediateTensors:
996 + if intermediate_tensors is not None:
997 + inputs_embeds = None
998 +
999 + hidden_states = self.language_model(
1000 + input_ids=input_ids,
1001 + positions=positions,
1002 + intermediate_tensors=intermediate_tensors,
1003 + inputs_embeds=inputs_embeds,
1004 + )
1005 + return hidden_states
1006 +
1007 + def compute_logits(
1008 + self,
1009 + hidden_states: torch.Tensor,
1010 + ) -> torch.Tensor | None:
1011 + return self.language_model.compute_logits(hidden_states)
1012 +
1013 + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1014 + loader = AutoWeightsLoader(
1015 + self,
1016 + skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
1017 + )
1018 + return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1019 +
1020 + def get_mm_mapping(self) -> MultiModelKeys:
1021 + """
1022 + Get the module prefix in multimodal models
1023 + """
1024 + return MultiModelKeys.from_string_field(
1025 + language_model="language_model.model",
1026 + connector="visual.perceive",
1027 + tower_model="visual",
1028 + )
vllm/model_executor/models/interfaces.py
+50 -1
@@ -1047,7 +1047,7 @@ class SupportsMRoPE(Protocol):
1047 supports_mrope: ClassVar[Literal[True]] = True
1048 """
1049 A flag that indicates this model supports M-RoPE.
1050 -
1050 +
1051 Note:
1052 There is no need to redefine this flag if this class is in the
1053 MRO of your model class.
@@ -1088,3 +1088,52 @@ def supports_mrope(
1088 model: type[object] | object,
1089 ) -> TypeIs[type[SupportsMRoPE]] | TypeIs[SupportsMRoPE]:
1090 return isinstance(model, SupportsMRoPE)
1091 +
1092 +
1093 +@runtime_checkable
1094 +class SupportsXDRoPE(Protocol):
1095 + """The interface required for all models that support XD-RoPE."""
1096 +
1097 + supports_xdrope: ClassVar[Literal[True]] = True
1098 + """
1099 + A flag that indicates this model supports XD-RoPE.
1100 +
1101 + Note:
1102 + There is no need to redefine this flag if this class is in the
1103 + XDRope of your model class.
1104 + """
1105 +
1106 + def get_xdrope_input_positions(
1107 + self,
1108 + input_tokens: list[int],
1109 + mm_features: list["MultiModalFeatureSpec"],
1110 + ) -> torch.Tensor:
1111 + """
1112 + Get XD-RoPE input positions and delta value for this specific model.
1113 +
1114 + This method should be implemented by each model that supports XD-RoPE
1115 + to provide model-specific logic for computing input positions.
1116 +
1117 + Args:
1118 + input_tokens: List of input token IDs
1119 + mm_features: Information about each multi-modal data item
1120 +
1121 + Returns:
1122 + llm_positions: Tensor of shape `[xdrope_dim, num_tokens]` with
1123 + 4D(P/W/H/T) or 3D(W/H/T) positions.
1124 + """
1125 + ...
1126 +
1127 +
1128 +@overload
1129 +def supports_xdrope(model: type[object]) -> TypeIs[type[SupportsXDRoPE]]: ...
1130 +
1131 +
1132 +@overload
1133 +def supports_xdrope(model: object) -> TypeIs[SupportsXDRoPE]: ...
1134 +
1135 +
1136 +def supports_xdrope(
1137 + model: type[object] | object,
1138 +) -> TypeIs[type[SupportsXDRoPE]] | TypeIs[SupportsXDRoPE]:
1139 + return isinstance(model, SupportsXDRoPE)
vllm/model_executor/models/registry.py
+4
@@ -287,6 +287,10 @@ _MULTIMODAL_MODELS = {
287 "GraniteSpeechForConditionalGeneration",
288 ),
289 "H2OVLChatModel": ("h2ovl", "H2OVLChatModel"),
290 + "HunYuanVLForConditionalGeneration": (
291 + "hunyuan_vision",
292 + "HunYuanVLForConditionalGeneration",
293 + ),
294 "InternVLChatModel": ("internvl", "InternVLChatModel"),
295 "NemotronH_Nano_VL_V2": ("nano_nemotron_vl", "NemotronH_Nano_VL_V2"),
296 "OpenCUAForConditionalGeneration": (
vllm/transformers_utils/config.py
+18
@@ -86,6 +86,7 @@ _CONFIG_REGISTRY: dict[str, type[PretrainedConfig]] = LazyConfigDict(
86 deepseek_vl_v2="DeepseekVLV2Config",
87 deepseek_v32="DeepseekV3Config",
88 flex_olmo="FlexOlmoConfig",
89 + hunyuan_vl="HunYuanVLConfig",
90 kimi_linear="KimiLinearConfig",
91 kimi_vl="KimiVLConfig",
92 RefinedWeb="RWConfig", # For tiiuae/falcon-40b(-instruct)
@@ -549,6 +550,23 @@ def thinker_uses_mrope(config: PretrainedConfig) -> bool:
550 return uses_mrope(thinker_text_config)
551
552
553 +def uses_xdrope_dim(config: PretrainedConfig) -> int:
554 + """Detect if the model with this config uses XD-ROPE."""
555 + xdrope_section = getattr(config, "xdrope_section", None)
556 + if xdrope_section is not None and isinstance(xdrope_section, list):
557 + return len(xdrope_section)
558 + rope_scaling = getattr(config, "rope_scaling", None)
559 + if rope_scaling is None:
560 + return 0
561 +
562 + if isinstance(rope_scaling, dict) and "xdrope_section" in rope_scaling:
563 + xdrope_section = rope_scaling["xdrope_section"]
564 + if xdrope_section is not None and isinstance(xdrope_section, list):
565 + return len(xdrope_section)
566 +
567 + return 0
568 +
569 +
570 def is_encoder_decoder(config: PretrainedConfig) -> bool:
571 """Detect if the model with this config is used as an encoder/decoder."""
572
vllm/transformers_utils/configs/__init__.py
+8
@@ -23,6 +23,11 @@ from vllm.transformers_utils.configs.eagle import EAGLEConfig
23 # `FalconConfig` class from the official HuggingFace transformers library.
24 from vllm.transformers_utils.configs.falcon import RWConfig
25 from vllm.transformers_utils.configs.flex_olmo import FlexOlmoConfig
26 +from vllm.transformers_utils.configs.hunyuan_vl import (
27 + HunYuanVLConfig,
28 + HunYuanVLTextConfig,
29 + HunYuanVLVisionConfig,
30 +)
31 from vllm.transformers_utils.configs.jais import JAISConfig
32 from vllm.transformers_utils.configs.kimi_linear import KimiLinearConfig
33 from vllm.transformers_utils.configs.kimi_vl import KimiVLConfig
@@ -53,6 +58,9 @@ __all__ = [
58 "DotsOCRConfig",
59 "EAGLEConfig",
60 "FlexOlmoConfig",
61 + "HunYuanVLConfig",
62 + "HunYuanVLTextConfig",
63 + "HunYuanVLVisionConfig",
64 "RWConfig",
65 "JAISConfig",
66 "Lfm2MoeConfig",
vllm/transformers_utils/configs/hunyuan_vl.py new
+322
@@ -0,0 +1,322 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +# adapted from https://github.com/ManaEstras/transformers/blob/v4.57.1.hyvl/src/transformers/models/hunyuan_vl/configuration_hunyuan_vl.py
5 +
6 +from transformers import PretrainedConfig
7 +
8 +
9 +class HunYuanVLVisionConfig(PretrainedConfig):
10 + model_type = "hunyuan_vl"
11 + base_config_key = "vision_config"
12 +
13 + def __init__(
14 + self,
15 + hidden_act="gelu",
16 + hidden_size=1152,
17 + intermediate_size=4304,
18 + interpolate_mode="bilinear",
19 + rms_norm_eps=1e-05,
20 + learnable_mlp_pooling_size=0,
21 + num_attention_heads=16,
22 + num_key_value_heads=None,
23 + num_channels=3,
24 + num_hidden_layers=27,
25 + out_hidden_size=4096,
26 + patch_size=16,
27 + remove_prenorm=True,
28 + spatial_merge_size=2,
29 + temporal_patch_size=1,
30 + resize_resolution=2048,
31 + img_max_token_num=4096,
32 + max_image_size=2048,
33 + video_max_image_size=768,
34 + video_min_image_size=256,
35 + min_image_size=512,
36 + anyres_vit_max_image_size=2048,
37 + max_vit_seq_len=16384,
38 + text_hidden_size=3072,
39 + **kwargs,
40 + ):
41 + super().__init__(**kwargs)
42 +
43 + self.hidden_act = hidden_act
44 + self.hidden_size = hidden_size
45 + self.intermediate_size = intermediate_size
46 + self.interpolate_mode = interpolate_mode
47 + self.learnable_mlp_pooling_size = learnable_mlp_pooling_size
48 + self.num_attention_heads = num_attention_heads
49 + if not num_key_value_heads:
50 + self.num_key_value_heads = num_attention_heads
51 + else:
52 + self.num_key_value_heads = num_key_value_heads
53 + self.num_channels = num_channels
54 + self.num_hidden_layers = num_hidden_layers
55 + self.out_hidden_size = out_hidden_size
56 + self.patch_size = patch_size
57 + self.remove_prenorm = remove_prenorm
58 + self.spatial_merge_size = spatial_merge_size
59 + self.temporal_patch_size = temporal_patch_size
60 + self.rms_norm_eps = rms_norm_eps
61 +
62 + self.resize_resolution = resize_resolution
63 + self.img_max_token_num = img_max_token_num
64 + self.max_image_size = max_image_size
65 + self.min_image_size = min_image_size
66 + self.video_max_image_size = video_max_image_size
67 + self.video_min_image_size = video_min_image_size
68 + self.anyres_vit_max_image_size = anyres_vit_max_image_size
69 + self.max_vit_seq_len = max_vit_seq_len
70 + self.text_hidden_size = text_hidden_size
71 +
72 +
73 +class HunYuanVLTextConfig(PretrainedConfig):
74 + r"""
75 + This is the configuration class to store the configuration of a [`HunYuanVLTextConfig`]. It is used to instantiate an
76 + HunYuan model according to the specified arguments, defining the model architecture. Instantiating a configuration
77 + with the defaults will yield a similar configuration to that of the HunYuan-7B.
78 + Hunyuan-7B-Instruct [tencent/Hunyuan-7B-Instruct](https://huggingface.co/tencent/Hunyuan-7B-Instruct).
79 +
80 + Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
81 + documentation from [`PretrainedConfig`] for more information.
82 +
83 +
84 + Args:
85 + vocab_size (`int`, *optional*, defaults to 290943):
86 + Vocabulary size of the HunYuan model. Defines the number of different tokens that can be represented by the
87 + `inputs_ids` passed when calling [`HunYuanVLTextConfig`]
88 + hidden_size (`int`, *optional*, defaults to 4096):
89 + Dimension of the hidden representations.
90 + intermediate_size (`int`, *optional*, defaults to 11008):
91 + Dimension of the MLP representations or shared MLP representations.
92 + num_hidden_layers (`int`, *optional*, defaults to 32):
93 + Number of hidden layers in the Transformer decoder.
94 + num_attention_heads (`int`, *optional*, defaults to 32):
95 + Number of attention heads for each attention layer in the Transformer decoder.
96 + num_key_value_heads (`int`, *optional*):
97 + This is the number of key_value heads that should be used to implement Grouped Query Attention. If
98 + `num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if
99 + `num_key_value_heads=1 the model will use Multi Query Attention (MQA) otherwise GQA is used. When
100 + converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed
101 + by meanpooling all the original heads within that group. For more details checkout [this
102 + paper](https://huggingface.co/papers/2305.13245). If it is not specified, will default to
103 + `num_attention_heads`.
104 + hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
105 + The non-linear activation function (function or string) in the decoder.
106 + max_position_embeddings (`int`, *optional*, defaults to 2048):
107 + The maximum sequence length that this model might ever be used with.
108 + initializer_range (`float`, *optional*, defaults to 0.02):
109 + The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
110 + rms_norm_eps (`float`, *optional*, defaults to 1e-05):
111 + The epsilon used by the rms normalization layers.
112 + use_cache (`bool`, *optional*, defaults to `True`):
113 + Whether or not the model should return the last key/values attentions (not used by all models). Only
114 + relevant if `config.is_decoder=True`.
115 + pad_token_id (`int`, *optional*, defaults to 0):
116 + Padding token id.
117 + bos_token_id (`int`, *optional*, defaults to 1):
118 + Beginning of stream token id.
119 + eos_token_id (`int`, *optional*, defaults to 2):
120 + End of stream token id.
121 + eod_token_id (int, *optional*, defaults to 3):
122 + Token ID representing the end-of-document marker. Used to indicate the termination of a text sequence.
123 + Example: In multi-document processing, this token helps the model distinguish between separate documents.
124 + pretraining_tp (`int`, *optional*, defaults to 1):
125 + Experimental feature. Tensor parallelism rank used during pretraining. Please refer to [this
126 + document](https://huggingface.co/docs/transformers/parallelism) to understand more about it. This value is
127 + necessary to ensure exact reproducibility of the pretraining results. Please refer to [this
128 + issue](https://github.com/pytorch/pytorch/issues/76232).
129 + tie_word_embeddings (`bool`, *optional*, defaults to `False`):
130 + Whether to tie weight embeddings
131 + rope_theta (`float`, *optional*, defaults to 10000.0):
132 + The base period of the RoPE embeddings.
133 + rope_scaling (`Dict`, *optional*):
134 + Dictionary containing the scaling configuration for the RoPE embeddings. Currently supports two scaling
135 + strategies: linear and dynamic. Their scaling factor must be a float greater than 1. The expected format is
136 + `{"type": strategy name, "factor": scaling factor}`. When using this flag, don't update
137 + `max_position_embeddings` to the expected new maximum. See the following thread for more information on how
138 + these scaling strategies behave:
139 + https://www.reddit.com/r/LocalLLaMA/comments/14mrgpr/dynamically_scaled_rope_further_increases/. This is an
140 + experimental feature, subject to breaking API changes in future versions.
141 + attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`):
142 + Whether to use a bias in the query, key, value and output projection layers during self-attention.
143 + attention_dropout (`float`, *optional*, defaults to 0.0):
144 + The dropout ratio for the attention probabilities.
145 + head_dim (`int`, *optional*, defaults to 128):
146 + The attention head dimension.
147 + """ # noqa: E501
148 +
149 + model_type = "hunyuan_vl_text"
150 + keys_to_ignore_at_inference = ["past_key_values"]
151 +
152 + def __init__(
153 + self,
154 + vocab_size=290943,
155 + hidden_size=4096,
156 + intermediate_size: int = 11008,
157 + num_hidden_layers=32,
158 + num_attention_heads=32,
159 + num_key_value_heads=None,
160 + hidden_act="silu",
161 + max_position_embeddings=2048,
162 + initializer_range=0.02,
163 + rms_norm_eps=1e-5,
164 + use_cache=True,
165 + pad_token_id=0,
166 + bos_token_id=1,
167 + eos_token_id=2,
168 + eod_token_id=3,
169 + pretraining_tp=1,
170 + tie_word_embeddings=False,
171 + rope_theta=10000.0,
172 + rope_scaling=None,
173 + attention_bias=False,
174 + attention_dropout=0.0,
175 + head_dim=None,
176 + **kwargs,
177 + ):
178 + self.vocab_size = vocab_size
179 + self.max_position_embeddings = max_position_embeddings
180 + self.hidden_size = hidden_size
181 + self.intermediate_size = intermediate_size
182 + self.num_hidden_layers = num_hidden_layers
183 + self.num_attention_heads = num_attention_heads
184 + self.head_dim = head_dim
185 + # for backward compatibility
186 + if num_key_value_heads is None:
187 + num_key_value_heads = num_attention_heads
188 +
189 + self.num_key_value_heads = num_key_value_heads
190 + self.hidden_act = hidden_act
191 + self.initializer_range = initializer_range
192 + self.rms_norm_eps = rms_norm_eps
193 + self.pretraining_tp = pretraining_tp
194 + self.use_cache = use_cache
195 + self.rope_theta = rope_theta
196 + self.rope_scaling = rope_scaling
197 + # self._rope_scaling_validation() # TODO: Need validation?
198 + self.attention_bias = attention_bias
199 + self.attention_dropout = attention_dropout
200 +
201 + super().__init__(
202 + pad_token_id=pad_token_id,
203 + bos_token_id=bos_token_id,
204 + eos_token_id=eos_token_id,
205 + tie_word_embeddings=tie_word_embeddings,
206 + **kwargs,
207 + )
208 +
209 + def _rope_scaling_validation(self):
210 + """
211 + Validate the `rope_scaling` configuration.
212 + """
213 + if self.rope_scaling is None:
214 + return
215 +
216 + if not isinstance(self.rope_scaling, dict) or len(self.rope_scaling) != 2:
217 + raise ValueError(
218 + "`rope_scaling` must be a dictionary with with two fields, `type` and "
219 + f"`factor` or `type` and `alpha`, got {self.rope_scaling}"
220 + )
221 + rope_scaling_type = self.rope_scaling.get("type", None)
222 + rope_scaling_factor = self.rope_scaling.get("factor", None)
223 + rope_scaling_alpha = self.rope_scaling.get("alpha", None)
224 + if rope_scaling_type is None or rope_scaling_type not in ["linear", "dynamic"]:
225 + raise ValueError(
226 + "`rope_scaling`'s type field must be one of ['linear', 'dynamic'], "
227 + f"got {rope_scaling_type}"
228 + )
229 + if rope_scaling_factor is None and rope_scaling_alpha is None:
230 + raise ValueError(
231 + "`rope_scaling`'s factor or alpha field must be have one, "
232 + "got both of none"
233 + )
234 + if rope_scaling_factor is not None and (
235 + not isinstance(rope_scaling_factor, float) or rope_scaling_factor <= 1.0
236 + ):
237 + raise ValueError(
238 + "`rope_scaling`'s factor field must be a float > 1.0, "
239 + f"got {rope_scaling_factor}"
240 + )
241 + if rope_scaling_alpha is not None and (
242 + not isinstance(rope_scaling_alpha, float) or rope_scaling_alpha <= 1.0
243 + ):
244 + raise ValueError(
245 + "`rope_scaling`'s alpha field must be a float > 1.0, "
246 + f"got {rope_scaling_alpha}"
247 + )
248 +
249 +
250 +class HunYuanVLConfig(PretrainedConfig):
251 + model_type = "hunyuan_vl"
252 + sub_configs = {
253 + "vision_config": HunYuanVLVisionConfig,
254 + "text_config": HunYuanVLTextConfig,
255 + }
256 + keys_to_ignore_at_inference = ["past_key_values"]
257 +
258 + def __init__(
259 + self,
260 + text_config=None,
261 + vision_config=None,
262 + im_start_id=120118,
263 + im_end_id=120119,
264 + image_token_id=120120,
265 + im_newline_id=120121,
266 + video_start_id=120122,
267 + video_end_id=120123,
268 + **kwargs,
269 + ):
270 + # We need to init super() here so that it does not reset values
271 + # that are in text config to the BaseClass defaults. The Base
272 + # config has many text related defaults and not all defaults are
273 + # same as for `HunYuanVLTextConfig`.
274 + super().__init__(**kwargs)
275 +
276 + if isinstance(vision_config, dict):
277 + self.vision_config = self.sub_configs["vision_config"](**vision_config)
278 + elif vision_config is None:
279 + self.vision_config = self.sub_configs["vision_config"]()
280 +
281 + if isinstance(text_config, dict):
282 + self.text_config = self.sub_configs["text_config"](**text_config)
283 + elif text_config is None:
284 + # For BC use all kwargs to init `TextConfig`
285 + self.text_config = self.sub_configs["text_config"](**kwargs)
286 +
287 + self.image_token_id = image_token_id
288 + self.im_start_id = im_start_id
289 + self.im_end_id = im_end_id
290 + self.im_newline_id = im_newline_id
291 + self.video_start_id = video_start_id
292 + self.video_end_id = video_end_id
293 +
294 + self.vision_config.text_hidden_size = self.text_config.hidden_size
295 +
296 + # Attention implementation to use. It sets it recursively on sub-configs
297 + # so we call it again in the end.
298 + self._attn_implementation = kwargs.pop("attn_implementation", None)
299 +
300 + def __setattr__(self, key, value):
301 + if (
302 + (text_config := super().__getattribute__("__dict__").get("text_config"))
303 + is not None
304 + and key not in ["dtype", "_attn_implementation_internal"]
305 + and key in text_config.__dict__
306 + ):
307 + setattr(text_config, key, value)
308 + else:
309 + super().__setattr__(key, value)
310 +
311 + def __getattribute__(self, key):
312 + if "text_config" in super().__getattribute__("__dict__") and key not in [
313 + "_name_or_path",
314 + "model_type",
315 + "dtype",
316 + "_attn_implementation_internal",
317 + ]:
318 + text_config = super().__getattribute__("text_config")
319 + if key in text_config.__dict__:
320 + return getattr(text_config, key)
321 +
322 + return super().__getattribute__(key)
vllm/transformers_utils/processors/__init__.py
+9 -1
@@ -9,7 +9,15 @@ reasons:
9 """
10
11 from vllm.transformers_utils.processors.deepseek_vl2 import DeepseekVLV2Processor
12 +from vllm.transformers_utils.processors.hunyuan_vl import HunYuanVLProcessor
13 +from vllm.transformers_utils.processors.hunyuan_vl_image import HunYuanVLImageProcessor
14 from vllm.transformers_utils.processors.ovis import OvisProcessor
15 from vllm.transformers_utils.processors.ovis2_5 import Ovis2_5Processor
16
15 -__all__ = ["DeepseekVLV2Processor", "OvisProcessor", "Ovis2_5Processor"]
17 +__all__ = [
18 + "DeepseekVLV2Processor",
19 + "HunYuanVLProcessor",
20 + "HunYuanVLImageProcessor",
21 + "OvisProcessor",
22 + "Ovis2_5Processor",
23 +]
vllm/transformers_utils/processors/hunyuan_vl.py new
+233
@@ -0,0 +1,233 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +# adapted from https://github.com/ManaEstras/transformers/blob/v4.57.1.hyvl/src/transformers/models/hunyuan_vl/processing_hunyuan_vl.py
5 +
6 +import numpy as np
7 +import torch
8 +from transformers import AutoProcessor
9 +from transformers.feature_extraction_utils import BatchFeature
10 +from transformers.image_utils import ImageInput
11 +from transformers.processing_utils import ProcessorMixin
12 +from transformers.tokenization_utils_base import PreTokenizedInput, TextInput
13 +from transformers.video_utils import VideoInput
14 +
15 +
16 +class HunYuanVLProcessor(ProcessorMixin):
17 + attributes = ["image_processor", "tokenizer"]
18 + valid_kwargs = ["chat_template"]
19 + image_processor_class = "AutoImageProcessor"
20 + tokenizer_class = "AutoTokenizer" # ("AutoTokenizer", None)
21 +
22 + def __init__(
23 + self,
24 + image_processor=None,
25 + tokenizer=None,
26 + video_processor=None,
27 + chat_template=None,
28 + **kwargs,
29 + ):
30 + # TODO Fix the init
31 + self.tokenizer = tokenizer
32 + self.image_token_id = 120120 # self.tokenizer.image_token_id
33 + self.image_token = self.tokenizer.convert_ids_to_tokens(self.image_token_id)
34 + self.im_start_token_id = 120118 # self.tokenizer.im_start_id
35 + self.im_start_token = self.tokenizer.convert_ids_to_tokens(
36 + self.im_start_token_id
37 + )
38 + self.im_end_token_id = 120119 # self.tokenizer.im_end_id
39 + self.im_end_token = self.tokenizer.convert_ids_to_tokens(self.im_end_token_id)
40 + self.placeholder_token = self.tokenizer.convert_ids_to_tokens(
41 + self.tokenizer.vocab_size - 1
42 + )
43 + self.pad_id = 120002 # self.tokenizer.pad_token_id
44 +
45 + super().__init__(
46 + image_processor, tokenizer, video_processor, chat_template=chat_template
47 + )
48 +
49 + def __call__(
50 + self,
51 + images: ImageInput = None,
52 + text: TextInput
53 + | PreTokenizedInput
54 + | list[TextInput]
55 + | list[PreTokenizedInput] = None,
56 + videos: VideoInput = None,
57 + **kwargs,
58 + ) -> BatchFeature:
59 + image_inputs = {}
60 + if images is not None:
61 + image_inputs = self.image_processor(images=images)
62 + image_grid_thw = image_inputs["image_grid_thw"]
63 +
64 + if not isinstance(text, list):
65 + text = [text]
66 +
67 + text = text.copy() # below lines change text in-place
68 +
69 + image_tokens_cumsum = [0]
70 + if images is not None:
71 + index = 0
72 + for i in range(len(text)):
73 + while self.image_token in text[i]:
74 + grid_h, grid_w = image_grid_thw[index][-2:]
75 + patch_h = grid_h // self.image_processor.merge_size
76 + patch_w = grid_w // self.image_processor.merge_size
77 + num_image_tokens = patch_h * (patch_w + 1) + 2
78 + image_tokens_cumsum.append(
79 + image_tokens_cumsum[-1] + num_image_tokens
80 + )
81 + # text[i] = text[i].replace(self.image_token, self.im_start_token + self.placeholder_token * num_image_tokens + self.im_end_token, 1) # noqa: E501
82 + text[i] = text[i].replace(
83 + self.image_token, self.placeholder_token * num_image_tokens, 1
84 + )
85 + index += 1
86 + text[i] = text[i].replace(self.placeholder_token, self.image_token)
87 + # text[i] = self.tokenizer.bos_token + text[i]
88 +
89 + text_inputs = self.tokenizer(text, add_special_tokens=False, **kwargs)
90 + self._check_special_mm_tokens(text, text_inputs, modalities=["image"])
91 +
92 + input_ids = text_inputs["input_ids"]
93 + position_ids = torch.arange(len(input_ids[0]))
94 + position_ids_w = torch.arange(len(input_ids[0]))
95 + position_ids_h = torch.arange(len(input_ids[0]))
96 + position_ids_t = torch.arange(len(input_ids[0]))
97 +
98 + if images is not None:
99 + image_token_pos_indices = torch.where(input_ids[0] == self.image_token_id)[
100 + 0
101 + ]
102 + for i in range(len(image_grid_thw)):
103 + grid_h, grid_w = image_grid_thw[i][-2:]
104 + patch_h = grid_h // self.image_processor.merge_size
105 + patch_w = grid_w // self.image_processor.merge_size
106 + start_pos = image_token_pos_indices[image_tokens_cumsum[i]].item() + 1
107 + replace_num = (patch_w + 1) * patch_h
108 + position_ids_w[start_pos : start_pos + replace_num] = torch.tensor(
109 + list(range(patch_w + 1)) * patch_h, dtype=torch.int64
110 + )
111 + patch_h_list = []
112 + for h in range(patch_h):
113 + patch_h_list += [h] * (patch_w + 1)
114 + position_ids_h[start_pos : start_pos + replace_num] = torch.tensor(
115 + patch_h_list, dtype=torch.int64
116 + )
117 + position_ids_t[start_pos : start_pos + replace_num] = 0
118 +
119 + position_ids = torch.stack(
120 + [position_ids, position_ids_w, position_ids_h, position_ids_t]
121 + ).unsqueeze(0)
122 + text_inputs["position_ids"] = position_ids
123 +
124 + attention_mask = input_ids.ne(self.pad_id)
125 + text_inputs["attention_mask"] = attention_mask
126 + text_inputs["imgs_pos"] = [self.get_imgs_pos(input_ids)]
127 + # image_inputs["imgs"] = [[image_inputs["pixel_values"]]]
128 +
129 + return_tensors = kwargs.pop("return_tensors", None)
130 + return BatchFeature(
131 + data={**text_inputs, **image_inputs},
132 + tensor_type=return_tensors,
133 + )
134 +
135 + def batch_decode(self, *args, **kwargs):
136 + return self.tokenizer.batch_decode(*args, **kwargs)
137 +
138 + def decode(self, *args, **kwargs):
139 + return self.tokenizer.decode(*args, **kwargs)
140 +
141 + def post_process_image_text_to_text(
142 + self,
143 + generated_outputs,
144 + skip_special_tokens=True,
145 + clean_up_tokenization_spaces=False,
146 + **kwargs,
147 + ):
148 + assert 0
149 +
150 + def apply_chat_template(self, *args, **kwargs):
151 + token_ids = self.tokenizer.apply_chat_template(*args, **kwargs)
152 + return token_ids
153 +
154 + def get_imgs_pos(self, doc_ids):
155 + doc_ids = np.array(doc_ids, dtype=np.int64)
156 + img_begin_index = np.where(doc_ids == self.im_start_token_id)[0]
157 + img_end_index = np.where(doc_ids == self.im_end_token_id)[0]
158 + imgs_pos = np.concatenate(
159 + (
160 + np.reshape(img_begin_index + 1, (-1, 1)),
161 + np.reshape(img_end_index, (-1, 1)),
162 + ),
163 + axis=-1,
164 + ).tolist()
165 + return imgs_pos
166 +
167 + @property
168 + def model_input_names(self):
169 + tokenizer_input_names = self.tokenizer.model_input_names
170 + image_processor_input_names = self.image_processor.model_input_names
171 + return list(dict.fromkeys(tokenizer_input_names + image_processor_input_names))
172 +
173 +
174 +def split_image_into_patch_blocks(
175 + pixel_values: torch.Tensor, # shape: [batch_size, 3, H, W]
176 + patch_size: int = 16, # e.g. 16
177 + adaptor_patch_div: int = 4, # e.g. 4 --> each patch_size is cut into 4x4 small regions, i.e. patch_size // 4 # noqa: E501
178 +) -> torch.Tensor:
179 + """
180 + Split the input image tensor (supporting batch) into large patches of size `patch_size`,
181 + and then further divide each large patch into smaller regions of size
182 + (patch_size // adaptor_patch_div) x (patch_size // adaptor_patch_div).
183 + Each small region is extracted as a tensor of shape [3, patch_size, patch_size].
184 + The final output contains all such small region tensors.
185 +
186 + Args:
187 + pixel_values: Input image tensor of shape [batch_size, 3, H, W].
188 + patch_size: Size of the large patch, e.g., 16.
189 + adaptor_patch_div: Each large patch is divided into
190 + (patch_size // adaptor_patch_div) x (patch_size // adaptor_patch_div)
191 + smaller regions.
192 +
193 + Returns:
194 + patches: A tensor of shape [N, 3, patch_size, patch_size],
195 + where N = batch_size * (H // patch_size) * (W // patch_size) * (patch_size // adaptor_patch_div)^2.
196 + Each element in the batch corresponds to one small image region.
197 + """ # noqa: E501
198 + batch_size, channels, height, width = pixel_values.shape
199 + assert channels == 3, "Pixel values must have 3 channels in dim=1"
200 + assert height % patch_size == 0 and width % patch_size == 0, (
201 + "H and W must be divisible by patch_size"
202 + )
203 +
204 + patch_height_num = height // patch_size
205 + patch_width_num = width // patch_size
206 +
207 + # Reshape to [B, 3, ph, ps, pw, ps]
208 + img = pixel_values.reshape(
209 + batch_size, 3, patch_height_num, patch_size, patch_width_num, patch_size
210 + )
211 +
212 + # Further split each psxps patch into (ps//aps)x(ps//aps) small regions
213 + img = img.reshape(
214 + batch_size,
215 + 3,
216 + patch_height_num,
217 + patch_size // adaptor_patch_div, # ps // aps
218 + adaptor_patch_div,
219 + patch_width_num,
220 + patch_size // adaptor_patch_div, # ps // aps
221 + adaptor_patch_div,
222 + )
223 +
224 + # Permute to group the small regions: [B, ph, pw, ps//aps, ps//aps, 3, aps, aps]
225 + img = img.permute(0, 2, 5, 3, 6, 1, 4, 7)
226 +
227 + # Reshape into [B * ph * pw * (ps//aps)^2, 3, patch_size, patch_size]
228 + patches = img.reshape(-1, 3, patch_size, patch_size)
229 +
230 + return patches
231 +
232 +
233 +AutoProcessor.register("HunYuanVLProcessor", HunYuanVLProcessor)
vllm/transformers_utils/processors/hunyuan_vl_image.py new
+477
@@ -0,0 +1,477 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +# adapted from https://github.com/ManaEstras/transformers/blob/v4.57.1.hyvl/src/transformers/models/hunyuan_vl/image_processing_hunyuan_vl.py
5 +"""Image processor class for HunYuanVL."""
6 +
7 +# isort conflicts with ruff for transformers imports
8 +# isort: skip_file
9 +import math
10 +
11 +import numpy as np
12 +import torchvision.transforms as transforms
13 +from transformers import AutoImageProcessor
14 +from transformers.image_processing_utils import BaseImageProcessor, BatchFeature
15 +from transformers.image_transforms import (
16 + convert_to_rgb,
17 +)
18 +from transformers.image_utils import (
19 + OPENAI_CLIP_MEAN,
20 + OPENAI_CLIP_STD,
21 + ChannelDimension,
22 + ImageInput,
23 + PILImageResampling,
24 + make_flat_list_of_images,
25 + make_list_of_images,
26 + valid_images,
27 + validate_preprocess_arguments,
28 +)
29 +from transformers.utils import TensorType, logging
30 +from transformers.video_utils import VideoInput, make_batched_videos
31 +
32 +logger = logging.get_logger(__name__)
33 +
34 +
35 +def smart_resize(
36 + height: int,
37 + width: int,
38 + factor: int = 16,
39 + min_pixels: int = 512 * 512,
40 + max_pixels: int = 2048 * 2048,
41 +):
42 + """Rescales the image so that the following conditions are met:
43 +
44 + 1. Both dimensions (height and width) are divisible by 'factor'.
45 +
46 + 2. The total number of pixels is within the range ['min_pixels', 'max_pixels'].
47 +
48 + 3. The aspect ratio of the image is maintained as closely as possible.
49 +
50 + """
51 + if max(height, width) / min(height, width) > 200:
52 + raise ValueError(
53 + "absolute aspect ratio must be smaller than 200, got "
54 + f"{max(height, width) / min(height, width)}"
55 + )
56 + h_bar = round(height / factor) * factor
57 + w_bar = round(width / factor) * factor
58 + if h_bar * w_bar > max_pixels:
59 + beta = math.sqrt((height * width) / max_pixels)
60 + h_bar = max(factor, math.floor(height / beta / factor) * factor)
61 + w_bar = max(factor, math.floor(width / beta / factor) * factor)
62 + elif h_bar * w_bar < min_pixels:
63 + beta = math.sqrt(min_pixels / (height * width))
64 + h_bar = math.ceil(height * beta / factor) * factor
65 + w_bar = math.ceil(width * beta / factor) * factor
66 + return h_bar, w_bar
67 +
68 +
69 +class HunYuanVLImageProcessor(BaseImageProcessor):
70 + model_input_names = [
71 + "pixel_values",
72 + "image_grid_thw",
73 + "pixel_values_videos",
74 + "video_grid_thw",
75 + ]
76 +
77 + def __init__(
78 + self,
79 + do_resize: bool = True,
80 + size: dict[str, int] | None = None,
81 + resample: PILImageResampling = PILImageResampling.BICUBIC,
82 + do_rescale: bool = True,
83 + rescale_factor: int | float = 1 / 255,
84 + do_normalize: bool = True,
85 + image_mean: float | list[float] | None = None,
86 + image_std: float | list[float] | None = None,
87 + do_convert_rgb: bool = True,
88 + min_pixels: int | None = None,
89 + max_pixels: int | None = None,
90 + patch_size: int = 16,
91 + temporal_patch_size: int = 2,
92 + merge_size: int = 2,
93 + **kwargs,
94 + ) -> None:
95 + super().__init__(**kwargs)
96 + if size is not None and (
97 + "shortest_edge" not in size or "longest_edge" not in size
98 + ):
99 + raise ValueError(
100 + "size must contain 'shortest_edge' and 'longest_edge' keys."
101 + )
102 + else:
103 + size = {"shortest_edge": 512 * 512, "longest_edge": 2048 * 2048}
104 + # backward compatibility: override size with min_pixels and max_pixels
105 + # if they are provided.
106 + if min_pixels is not None:
107 + size["shortest_edge"] = min_pixels
108 + if max_pixels is not None:
109 + size["longest_edge"] = max_pixels
110 + self.min_pixels = size["shortest_edge"]
111 + self.max_pixels = size["longest_edge"]
112 + self.size = size
113 +
114 + self.do_resize = do_resize
115 + self.resample = resample
116 + self.do_rescale = do_rescale
117 + self.rescale_factor = rescale_factor
118 + self.do_normalize = do_normalize
119 + self.image_mean = image_mean if image_mean is not None else OPENAI_CLIP_MEAN
120 + self.image_std = image_std if image_std is not None else OPENAI_CLIP_STD
121 +
122 + self.patch_size = patch_size
123 + self.temporal_patch_size = temporal_patch_size
124 + self.merge_size = merge_size
125 + self.do_convert_rgb = do_convert_rgb
126 +
127 + # hard-code
128 +
129 + def _preprocess(
130 + self,
131 + images: ImageInput | VideoInput,
132 + do_resize: bool | None = None,
133 + size: dict[str, int] | None = None,
134 + resample: PILImageResampling = None,
135 + do_rescale: bool | None = None,
136 + rescale_factor: float | None = None,
137 + do_normalize: bool | None = None,
138 + image_mean: float | list[float] | None = None,
139 + image_std: float | list[float] | None = None,
140 + patch_size: int = 16,
141 + temporal_patch_size: int = 2,
142 + merge_size: int = 2,
143 + do_convert_rgb: bool | None = None,
144 + data_format: ChannelDimension | None = ChannelDimension.FIRST,
145 + input_data_format: str | ChannelDimension | None = None,
146 + ):
147 + """
148 + Preprocess an image or batch of images. Copy of the `preprocess` method from `CLIPImageProcessor`.
149 +
150 + Args:
151 + images (`ImageInput`):
152 + Image or batch of images to preprocess. Expects pixel values ranging from 0 to 255. If pixel values range from 0 to 1, set `do_rescale=False`.
153 + do_resize (`bool`, *optional*, defaults to `self.do_resize`):
154 + Whether to resize the image.
155 + size (`dict[str, int]`, *optional*, defaults to `self.size`):
156 + Size of the image after resizing. `shortest_edge` and `longest_edge` keys must be present.
157 + resample (`PILImageResampling`, *optional*, defaults to `self.resample`):
158 + Resampling filter to use if resizing the image. This can be one of the `PILImageResampling` enums.
159 + do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
160 + Whether to rescale the image.
161 + rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
162 + Scale factor to use if rescaling the image.
163 + do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
164 + Whether to normalize the image.
165 + image_mean (`float` or `list[float]`, *optional*, defaults to `self.image_mean`):
166 + Mean to use if normalizing the image. Can be a float or a list of floats corresponding to the number of channels in the image.
167 + image_std (`float` or `list[float]`, *optional*, defaults to `self.image_std`):
168 + Standard deviation to use if normalizing the image. Can be a float or a list of floats corresponding to the number of channels in the image.
169 + patch_size (`int`, *optional*, defaults to `self.patch_size`):
170 + The spatial patch size of the vision encoder.
171 + temporal_patch_size (`int`, *optional*, defaults to `self.temporal_patch_size`):
172 + The temporal patch size of the vision encoder.
173 + merge_size (`int`, *optional*, defaults to `self.merge_size`):
174 + The merge size of the vision encoder to llm encoder.
175 + do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):
176 + Whether to convert the image to RGB.
177 + data_format (`ChannelDimension`, *optional*, defaults to `ChannelDimension.FIRST`):
178 + The channel dimension format for the output image. Can be one of:
179 + - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
180 + - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
181 + - Unset: Use the channel dimension format of the input image.
182 + input_data_format (`ChannelDimension` or `str`, *optional*):
183 + The channel dimension format for the input image. Can be one of:
184 + - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
185 + - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
186 + - `"none"` or `ChannelDimension.NONE`: image in (height, width) format. - `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
187 + """ # noqa: E501
188 + images = make_list_of_images(images)
189 +
190 + if do_convert_rgb:
191 + images = [convert_to_rgb(image) for image in images]
192 +
193 + width, height = images[0].width, images[0].height
194 + resized_width, resized_height = width, height
195 + processed_images = []
196 + for image in images:
197 + if do_resize:
198 + resized_width, resized_height = smart_resize(
199 + width,
200 + height,
201 + factor=patch_size * merge_size,
202 + min_pixels=self.min_pixels,
203 + max_pixels=self.max_pixels,
204 + )
205 + image = image.resize((resized_width, resized_height))
206 +
207 + if do_normalize:
208 + image = transforms.Compose(
209 + [
210 + transforms.ToTensor(),
211 + transforms.Normalize(self.image_mean, self.image_std),
212 + ]
213 + )(image)
214 + processed_images.append(image)
215 +
216 + patches = np.array(processed_images)
217 + channel = patches.shape[1]
218 + grid_t = patches.shape[0] // temporal_patch_size
219 + grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
220 + patches = patches.reshape(
221 + 1,
222 + channel,
223 + grid_h // merge_size,
224 + merge_size,
225 + patch_size,
226 + grid_w // merge_size,
227 + merge_size,
228 + patch_size,
229 + )
230 + patches = patches.transpose(0, 2, 3, 5, 6, 1, 4, 7)
231 + flatten_patches = patches.reshape(
232 + 1 * grid_h * grid_w, channel * patch_size * patch_size
233 + )
234 +
235 + return flatten_patches, (grid_t, grid_h, grid_w)
236 +
237 + def preprocess(
238 + self,
239 + images: ImageInput,
240 + videos: VideoInput = None,
241 + do_resize: bool | None = None,
242 + size: dict[str, int] | None = None,
243 + min_pixels: int | None = None,
244 + max_pixels: int | None = None,
245 + resample: PILImageResampling = None,
246 + do_rescale: bool | None = None,
247 + rescale_factor: float | None = None,
248 + do_normalize: bool | None = None,
249 + image_mean: float | list[float] | None = None,
250 + image_std: float | list[float] | None = None,
251 + patch_size: int | None = None,
252 + temporal_patch_size: int | None = None,
253 + merge_size: int | None = None,
254 + do_convert_rgb: bool | None = None,
255 + return_tensors: str | TensorType | None = None,
256 + data_format: ChannelDimension | None = ChannelDimension.FIRST,
257 + input_data_format: str | ChannelDimension | None = None,
258 + ):
259 + """
260 + Args:
261 + images (`ImageInput`):
262 + Image to preprocess. Expects a single or batch of images with pixel values ranging from 0 to 255. If
263 + passing in images with pixel values between 0 and 1, set `do_rescale=False`.
264 + videos (`VideoInput`):
265 + Video to preprocess. Expects a single or batch of videos with pixel values ranging from 0 to 255. If
266 + passing in videos with pixel values between 0 and 1, set `do_rescale=False`.
267 + do_resize (`bool`, *optional*, defaults to `self.do_resize`):
268 + Whether to resize the image.
269 + size (`dict[str, int]`, *optional*, defaults to `self.size`):
270 + Size of the image after resizing. Shortest edge of the image is resized to size["shortest_edge"], with
271 + the longest edge resized to keep the input aspect ratio.
272 + resample (`int`, *optional*, defaults to `self.resample`):
273 + Resampling filter to use if resizing the image. This can be one of the enum `PILImageResampling`. Only
274 + has an effect if `do_resize` is set to `True`.
275 + do_rescale (`bool`, *optional*, defaults to `self.do_rescale`):
276 + Whether to rescale the image.
277 + rescale_factor (`float`, *optional*, defaults to `self.rescale_factor`):
278 + Rescale factor to rescale the image by if `do_rescale` is set to `True`.
279 + do_normalize (`bool`, *optional*, defaults to `self.do_normalize`):
280 + Whether to normalize the image.
281 + image_mean (`float` or `list[float]`, *optional*, defaults to `self.image_mean`):
282 + Image mean to use for normalization. Only has an effect if `do_normalize` is set to `True`.
283 + image_std (`float` or `list[float]`, *optional*, defaults to `self.image_std`):
284 + Image standard deviation to use for normalization. Only has an effect if `do_normalize` is set to
285 + `True`.
286 + min_pixels (`int`, *optional*, defaults to `self.min_pixels`):
287 + The min pixels of the image to resize the image.
288 + max_pixels (`int`, *optional*, defaults to `self.max_pixels`):
289 + The max pixels of the image to resize the image.
290 + patch_size (`int`, *optional*, defaults to `self.patch_size`):
291 + The spatial patch size of the vision encoder.
292 + temporal_patch_size (`int`, *optional*, defaults to `self.temporal_patch_size`):
293 + The temporal patch size of the vision encoder.
294 + merge_size (`int`, *optional*, defaults to `self.merge_size`):
295 + The merge size of the vision encoder to llm encoder.
296 + do_convert_rgb (`bool`, *optional*, defaults to `self.do_convert_rgb`):
297 + Whether to convert the image to RGB.
298 + return_tensors (`str` or `TensorType`, *optional*):
299 + The type of tensors to return. Can be one of:
300 + - Unset: Return a list of `np.ndarray`.
301 + - `TensorType.TENSORFLOW` or `'tf'`: Return a batch of type `tf.Tensor`.
302 + - `TensorType.PYTORCH` or `'pt'`: Return a batch of type `torch.Tensor`.
303 + - `TensorType.NUMPY` or `'np'`: Return a batch of type `np.ndarray`.
304 + - `TensorType.JAX` or `'jax'`: Return a batch of type `jax.numpy.ndarray`.
305 + data_format (`ChannelDimension` or `str`, *optional*, defaults to `ChannelDimension.FIRST`):
306 + The channel dimension format for the output image. Can be one of:
307 + - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
308 + - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
309 + - Unset: Use the channel dimension format of the input image.
310 + input_data_format (`ChannelDimension` or `str`, *optional*):
311 + The channel dimension format for the input image. If unset, the channel dimension format is inferred
312 + from the input image. Can be one of:
313 + - `"channels_first"` or `ChannelDimension.FIRST`: image in (num_channels, height, width) format.
314 + - `"channels_last"` or `ChannelDimension.LAST`: image in (height, width, num_channels) format.
315 + - `"none"` or `ChannelDimension.NONE`: image in (height, width) format.
316 +
317 + """ # noqa: E501
318 + min_pixels = min_pixels if min_pixels is not None else self.min_pixels
319 + max_pixels = max_pixels if max_pixels is not None else self.max_pixels
320 +
321 + if size is not None:
322 + if "shortest_edge" not in size or "longest_edge" not in size:
323 + raise ValueError(
324 + "size must contain 'shortest_edge' and 'longest_edge' keys."
325 + )
326 + min_pixels = size["shortest_edge"]
327 + elif min_pixels is not None and max_pixels is not None:
328 + # backward compatibility: override size with min_pixels and max_pixels
329 + # if they are provided.
330 + size = {"shortest_edge": min_pixels, "longest_edge": max_pixels}
331 + else:
332 + size = {**self.size}
333 +
334 + do_resize = do_resize if do_resize is not None else self.do_resize
335 +
336 + resample = resample if resample is not None else self.resample
337 + do_rescale = do_rescale if do_rescale is not None else self.do_rescale
338 + rescale_factor = (
339 + rescale_factor if rescale_factor is not None else self.rescale_factor
340 + )
341 + do_normalize = do_normalize if do_normalize is not None else self.do_normalize
342 + image_mean = image_mean if image_mean is not None else self.image_mean
343 + image_std = image_std if image_std is not None else self.image_std
344 + patch_size = patch_size if patch_size is not None else self.patch_size
345 + temporal_patch_size = (
346 + temporal_patch_size
347 + if temporal_patch_size is not None
348 + else self.temporal_patch_size
349 + )
350 + merge_size = merge_size if merge_size is not None else self.merge_size
351 + do_convert_rgb = (
352 + do_convert_rgb if do_convert_rgb is not None else self.do_convert_rgb
353 + )
354 +
355 + if images is not None:
356 + images = make_flat_list_of_images(images)
357 +
358 + if images is not None and not valid_images(images):
359 + raise ValueError(
360 + "Invalid image type. Must be of type PIL.Image.Image, numpy.ndarray, "
361 + "torch.Tensor, tf.Tensor or jax.ndarray."
362 + )
363 +
364 + validate_preprocess_arguments(
365 + rescale_factor=rescale_factor,
366 + do_normalize=do_normalize,
367 + image_mean=image_mean,
368 + image_std=image_std,
369 + do_resize=do_resize,
370 + size=size,
371 + resample=resample,
372 + )
373 +
374 + data = {}
375 + if images is not None:
376 + pixel_values, vision_grid_thws = [], []
377 + for image in images:
378 + patches, image_grid_thw = self._preprocess(
379 + image,
380 + do_resize=do_resize,
381 + size=size,
382 + resample=resample,
383 + do_rescale=do_rescale,
384 + rescale_factor=rescale_factor,
385 + do_normalize=do_normalize,
386 + image_mean=image_mean,
387 + image_std=image_std,
388 + patch_size=patch_size,
389 + temporal_patch_size=temporal_patch_size,
390 + merge_size=merge_size,
391 + data_format=data_format,
392 + do_convert_rgb=do_convert_rgb,
393 + input_data_format=input_data_format,
394 + )
395 + pixel_values.extend(patches)
396 + vision_grid_thws.append(image_grid_thw)
397 + pixel_values = np.array(pixel_values)
398 + vision_grid_thws = np.array(vision_grid_thws)
399 + data.update(
400 + {"pixel_values": pixel_values, "image_grid_thw": vision_grid_thws}
401 + )
402 +
403 + # kept for BC only and should be removed after v5.0
404 + if videos is not None:
405 + logger.warning(
406 + "`HunYuanVLV1ImageProcessor` works only with image inputs "
407 + "and doesn't process videos anymore. "
408 + "This is a deprecated behavior and will be removed in v5.0. "
409 + "Your videos should be forwarded to `HunYuanVLV1VideoProcessor`. "
410 + )
411 + videos = make_batched_videos(videos)
412 + pixel_values_videos, vision_grid_thws_videos = [], []
413 + for images in videos:
414 + patches, video_grid_thw = self._preprocess(
415 + images,
416 + do_resize=do_resize,
417 + size=size,
418 + resample=resample,
419 + do_rescale=do_rescale,
420 + rescale_factor=rescale_factor,
421 + do_normalize=do_normalize,
422 + image_mean=image_mean,
423 + image_std=image_std,
424 + patch_size=patch_size,
425 + temporal_patch_size=temporal_patch_size,
426 + merge_size=merge_size,
427 + data_format=data_format,
428 + do_convert_rgb=do_convert_rgb,
429 + input_data_format=input_data_format,
430 + )
431 + pixel_values_videos.extend(patches)
432 + vision_grid_thws_videos.append(video_grid_thw)
433 + data.update(
434 + {
435 + "pixel_values_videos": np.array(pixel_values_videos),
436 + "video_grid_thw": np.array(vision_grid_thws_videos),
437 + }
438 + )
439 +
440 + return BatchFeature(data=data, tensor_type=return_tensors)
441 +
442 + def get_number_of_image_patches(self, height: int, width: int, images_kwargs=None):
443 + """
444 + A utility that returns number of image patches for a given image size.
445 +
446 + Args:
447 + height (`int`):
448 + Height of the input image.
449 + width (`int`):
450 + Width of the input image.
451 + images_kwargs (`dict`, *optional*):
452 + Any kwargs to override defaults of the image processor.
453 + Returns:
454 + `int`: Number of image patches per image.
455 + """
456 + min_pixels = (
457 + images_kwargs["min_pixels"]
458 + if "min_pixels" in images_kwargs
459 + else self.size["shortest_edge"]
460 + )
461 + max_pixels = (
462 + images_kwargs["max_pixels"]
463 + if "max_pixels" in images_kwargs
464 + else self.size["longest_edge"]
465 + )
466 + patch_size = images_kwargs.get("patch_size", self.patch_size)
467 + merge_size = images_kwargs.get("merge_size", self.merge_size)
468 +
469 + factor = patch_size * merge_size
470 + resized_height, resized_width = smart_resize(
471 + height, width, factor, min_pixels=min_pixels, max_pixels=max_pixels
472 + )
473 + grid_h, grid_w = resized_height // patch_size, resized_width // patch_size
474 + return grid_h * (grid_w + 1) + 2
475 +
476 +
477 +AutoImageProcessor.register("HunYuanVLImageProcessor", HunYuanVLImageProcessor)
vllm/v1/worker/gpu_input_batch.py
+2
@@ -43,6 +43,8 @@ class CachedRequestState:
43 mrope_positions: torch.Tensor | None = None
44 mrope_position_delta: int | None = None
45
46 + xdrope_positions: torch.Tensor | None = None
47 +
48 lora_request: LoRARequest | None = None
49 prompt_embeds: torch.Tensor | None = None
50
vllm/v1/worker/gpu_model_runner.py
+103 -1
@@ -50,16 +50,21 @@ from vllm.distributed.parallel_state import (
50 from vllm.forward_context import BatchDescriptor, set_forward_context
51 from vllm.logger import init_logger
52 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
53 -from vllm.model_executor.layers.rotary_embedding import MRotaryEmbedding
53 +from vllm.model_executor.layers.rotary_embedding import (
54 + MRotaryEmbedding,
55 + XDRotaryEmbedding,
56 +)
57 from vllm.model_executor.model_loader import TensorizerLoader, get_model_loader
58 from vllm.model_executor.models.interfaces import (
59 SupportsMRoPE,
60 SupportsMultiModal,
61 + SupportsXDRoPE,
62 is_mixture_of_experts,
63 supports_eagle3,
64 supports_mrope,
65 supports_multimodal_pruning,
66 supports_transcription,
67 + supports_xdrope,
68 )
69 from vllm.model_executor.models.interfaces_base import (
70 VllmModelForPooling,
@@ -324,6 +329,7 @@ class GPUModelRunner(
329 # Multi-modal data support
330 self.mm_registry = MULTIMODAL_REGISTRY
331 self.uses_mrope = model_config.uses_mrope
332 + self.uses_xdrope_dim = model_config.uses_xdrope_dim
333 self.supports_mm_inputs = self.mm_registry.supports_multimodal_inputs(
334 model_config
335 )
@@ -512,6 +518,13 @@ class GPUModelRunner(
518 (3, self.max_num_tokens + 1), dtype=torch.int64
519 )
520
521 + # Only relevant for models using XD-RoPE (e.g, HunYuan-VL)
522 + if self.uses_xdrope_dim > 0:
523 + # Similar to mrope but use assigned dimension number for RoPE, 4 as default.
524 + self.xdrope_positions = self._make_buffer(
525 + (self.uses_xdrope_dim, self.max_num_tokens + 1), dtype=torch.int64
526 + )
527 +
528 # None in the first PP rank. The rest are set after load_model.
529 self.intermediate_tensors: IntermediateTensors | None = None
530
@@ -593,10 +606,14 @@ class GPUModelRunner(
606 if isinstance(num_tokens, int):
607 if self.uses_mrope:
608 return self.mrope_positions.gpu[:, :num_tokens]
609 + if self.uses_xdrope_dim > 0:
610 + return self.xdrope_positions.gpu[:, :num_tokens]
611 return self.positions.gpu[:num_tokens]
612 else:
613 if self.uses_mrope:
614 return self.mrope_positions.gpu[:, num_tokens]
615 + if self.uses_xdrope_dim > 0:
616 + return self.xdrope_positions.gpu[:, num_tokens]
617 return self.positions.gpu[num_tokens]
618
619 def _make_buffer(
@@ -772,6 +789,10 @@ class GPUModelRunner(
789 if self.uses_mrope:
790 self._init_mrope_positions(req_state)
791
792 + # Only relevant for models using XD-RoPE (e.g, HunYuan-VL)
793 + if self.uses_xdrope_dim > 0:
794 + self._init_xdrope_positions(req_state)
795 +
796 reqs_to_add.append(req_state)
797
798 # Update the states of the running/resumed requests.
@@ -987,6 +1008,19 @@ class GPUModelRunner(
1008 )
1009 )
1010
1011 + def _init_xdrope_positions(self, req_state: CachedRequestState):
1012 + model = self.get_model()
1013 + xdrope_model = cast(SupportsXDRoPE, model)
1014 + assert req_state.prompt_token_ids is not None, (
1015 + "XD-RoPE requires prompt_token_ids to be available."
1016 + )
1017 + assert supports_xdrope(model), "XD-RoPE support is not implemented."
1018 +
1019 + req_state.xdrope_positions = xdrope_model.get_xdrope_input_positions(
1020 + req_state.prompt_token_ids,
1021 + req_state.mm_features,
1022 + )
1023 +
1024 def _extract_mm_kwargs(
1025 self,
1026 scheduler_output: "SchedulerOutput",
@@ -1231,6 +1265,11 @@ class GPUModelRunner(
1265 if self.uses_mrope:
1266 self._calc_mrope_positions(scheduler_output)
1267
1268 + # Calculate XD-RoPE positions.
1269 + # Only relevant for models using XD-RoPE (e.g, HunYuan-VL)
1270 + if self.uses_xdrope_dim > 0:
1271 + self._calc_xdrope_positions(scheduler_output)
1272 +
1273 # Get token indices.
1274 # E.g., [0, 1, 0, 1, 2, 3, 4, 0, 1, 2]
1275 # -> [0, 1, M, M + 1, M + 2, M + 3, M + 4, 2 * M, 2 * M + 1, 2 * M + 2]
@@ -1364,6 +1403,12 @@ class GPUModelRunner(
1403 self.mrope_positions.cpu[:, :total_num_scheduled_tokens],
1404 non_blocking=True,
1405 )
1406 + elif self.uses_xdrope_dim > 0:
1407 + # Only relevant for models using XD-RoPE (e.g, HunYuan-VL)
1408 + self.xdrope_positions.gpu[:, :total_num_scheduled_tokens].copy_(
1409 + self.xdrope_positions.cpu[:, :total_num_scheduled_tokens],
1410 + non_blocking=True,
1411 + )
1412 else:
1413 # Common case (1D positions)
1414 self.positions.copy_to_gpu(total_num_scheduled_tokens)
@@ -1793,6 +1838,53 @@ class GPUModelRunner(
1838
1839 mrope_pos_ptr += completion_part_len
1840
1841 + def _calc_xdrope_positions(self, scheduler_output: "SchedulerOutput"):
1842 + xdrope_pos_ptr = 0
1843 + for index, req_id in enumerate(self.input_batch.req_ids):
1844 + req = self.requests[req_id]
1845 + assert req.xdrope_positions is not None
1846 +
1847 + num_computed_tokens = self.input_batch.num_computed_tokens_cpu[index]
1848 + num_scheduled_tokens = scheduler_output.num_scheduled_tokens[req_id]
1849 + num_prompt_tokens = length_from_prompt_token_ids_or_embeds(
1850 + req.prompt_token_ids, req.prompt_embeds
1851 + )
1852 +
1853 + if num_computed_tokens + num_scheduled_tokens > num_prompt_tokens:
1854 + prompt_part_len = max(0, num_prompt_tokens - num_computed_tokens)
1855 + completion_part_len = max(0, num_scheduled_tokens - prompt_part_len)
1856 + else:
1857 + prompt_part_len = num_scheduled_tokens
1858 + completion_part_len = 0
1859 +
1860 + assert num_scheduled_tokens == prompt_part_len + completion_part_len
1861 +
1862 + if prompt_part_len > 0:
1863 + # prompt's xdrope_positions are pre-computed
1864 + dst_start = xdrope_pos_ptr
1865 + dst_end = xdrope_pos_ptr + prompt_part_len
1866 + src_start = num_computed_tokens
1867 + src_end = num_computed_tokens + prompt_part_len
1868 +
1869 + self.xdrope_positions.cpu[:, dst_start:dst_end] = req.xdrope_positions[
1870 + :, src_start:src_end
1871 + ]
1872 + xdrope_pos_ptr += prompt_part_len
1873 +
1874 + if completion_part_len > 0:
1875 + # compute completion's xdrope_positions on-the-fly
1876 + dst_start = xdrope_pos_ptr
1877 + dst_end = xdrope_pos_ptr + completion_part_len
1878 +
1879 + XDRotaryEmbedding.get_next_input_positions_tensor(
1880 + out=self.xdrope_positions.np,
1881 + out_offset=dst_start,
1882 + context_len=num_computed_tokens + prompt_part_len,
1883 + num_new_tokens=completion_part_len,
1884 + )
1885 +
1886 + xdrope_pos_ptr += completion_part_len
1887 +
1888 def _calc_spec_decode_metadata(
1889 self,
1890 num_draft_tokens: np.ndarray,
@@ -2037,6 +2129,7 @@ class GPUModelRunner(
2129
2130 req_start_idx = 0
2131 should_sync_mrope_positions = False
2132 + should_sync_xdrope_positions = False
2133
2134 for req_id in self.input_batch.req_ids:
2135 mm_embeds_req: list[torch.Tensor] = []
@@ -2110,6 +2203,10 @@ class GPUModelRunner(
2203 self._calc_mrope_positions(scheduler_output)
2204 self.mrope_positions.copy_to_gpu(total_num_scheduled_tokens)
2205
2206 + if should_sync_xdrope_positions:
2207 + self._calc_xdrope_positions(scheduler_output)
2208 + self.xdrope_positions.copy_to_gpu(total_num_scheduled_tokens)
2209 +
2210 return mm_embeds, is_mm_embed
2211
2212 def get_model(self) -> nn.Module:
@@ -2384,8 +2481,11 @@ class GPUModelRunner(
2481 input_ids = self.input_ids.gpu[:num_input_tokens]
2482 inputs_embeds = None
2483 model_kwargs = self._init_model_kwargs(num_input_tokens)
2484 +
2485 if self.uses_mrope:
2486 positions = self.mrope_positions.gpu[:, :num_input_tokens]
2487 + elif self.uses_xdrope_dim > 0:
2488 + positions = self.xdrope_positions.gpu[:, :num_input_tokens]
2489 else:
2490 positions = self.positions.gpu[:num_input_tokens]
2491
@@ -3824,6 +3924,8 @@ class GPUModelRunner(
3924
3925 if self.uses_mrope:
3926 positions = self.mrope_positions.gpu[:, :num_tokens_after_padding]
3927 + elif self.uses_xdrope_dim > 0:
3928 + positions = self.xdrope_positions.gpu[:, :num_tokens_after_padding]
3929 else:
3930 positions = self.positions.gpu[:num_tokens_after_padding]
3931