main
py 446 lines 15.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Inference-only Deepseek-OCR model compatible with HuggingFace weights."""
4
5 import math
6 from collections.abc import Iterable, Mapping, Sequence
7 from functools import partial
8
9 import torch
10 import torch.nn as nn
11 from transformers import BatchFeature
12
13 from vllm.config import VllmConfig
14 from vllm.config.multimodal import MultiModalDummyOptions
15 from vllm.inputs import MultiModalDataDict
16 from vllm.model_executor.models.interfaces import (
17 MultiModalEmbeddings,
18 SupportsLoRA,
19 SupportsMultiModal,
20 SupportsPP,
21 )
22 from vllm.model_executor.models.module_mapping import MultiModelKeys
23 from vllm.model_executor.models.utils import (
24 AutoWeightsLoader,
25 WeightsMapper,
26 init_vllm_registered_model,
27 maybe_prefix,
28 )
29 from vllm.multimodal import MULTIMODAL_REGISTRY
30 from vllm.multimodal.inputs import (
31 MultiModalFieldConfig,
32 MultiModalKwargsItems,
33 NestedTensors,
34 )
35 from vllm.multimodal.parse import (
36 ImageEmbeddingItems,
37 ImageProcessorItems,
38 ImageSize,
39 MultiModalDataItems,
40 )
41 from vllm.multimodal.processing import (
42 BaseDummyInputsBuilder,
43 BaseMultiModalProcessor,
44 BaseProcessingInfo,
45 PromptReplacement,
46 PromptUpdate,
47 )
48 from vllm.multimodal.processing.processor import HFMultiModalInputs
49 from vllm.sequence import IntermediateTensors
50 from vllm.tokenizers import cached_tokenizer_from_config
51 from vllm.transformers_utils.configs.deepseek_vl2 import DeepseekVLV2Config
52 from vllm.transformers_utils.processors.deepseek_ocr import (
53 BASE_SIZE,
54 CROP_MODE,
55 DeepseekOCRProcessor,
56 )
57
58 from ...transformers_utils.processors.deepseek_ocr import count_tiles
59 from .deepencoder import ImageEncoderViT
60 from .deepencoder2 import build_qwen2_decoder_as_encoder
61 from .deepseek_ocr import DeepseekOCRImagePixelInputs
62 from .deepseek_vl2 import MlpProjector
63
64 # The image token id may be various
65 IMAGE_SIZE = 768 # different from deepseek-ocr
66 _IMAGE_TOKEN = "<image>"
67
68
69 class DeepseekOCR2ProcessingInfo(BaseProcessingInfo):
70 def get_hf_config(self):
71 return self.ctx.get_hf_config(DeepseekVLV2Config)
72
73 def get_hf_processor(self, **kwargs: object):
74 v2_processor_config = dict(
75 image_size=IMAGE_SIZE,
76 base_size=BASE_SIZE,
77 crop_mode=CROP_MODE,
78 strategy="v2",
79 )
80
81 return self.ctx.get_hf_processor(
82 DeepseekOCRProcessor,
83 **{**v2_processor_config, **kwargs},
84 )
85
86 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
87 return {"image": None}
88
89 def get_num_image_tokens(
90 self, *, image_width: int, image_height: int, cropping: bool = True
91 ) -> int:
92 image_size = IMAGE_SIZE
93 base_size = BASE_SIZE
94 patch_size = 16
95 downsample_ratio = 4
96
97 if CROP_MODE:
98 if image_width <= 768 and image_height <= 768:
99 crop_ratio = [1, 1]
100 else:
101 # find the closest aspect ratio to the target
102 crop_ratio = count_tiles(
103 image_width, image_height, image_size=IMAGE_SIZE
104 )
105
106 num_width_tiles, num_height_tiles = crop_ratio
107 else:
108 num_width_tiles = num_height_tiles = 1
109
110 h = w = math.ceil((base_size // patch_size) / downsample_ratio)
111
112 h2 = w2 = math.ceil((image_size // patch_size) / downsample_ratio)
113
114 global_views_tokens = h * w
115 if num_width_tiles > 1 or num_height_tiles > 1:
116 local_views_tokens = (num_height_tiles * h2) * (num_width_tiles * w2)
117 else:
118 local_views_tokens = 0
119
120 return global_views_tokens + local_views_tokens + 1
121
122 def get_image_size_with_most_features(self) -> ImageSize:
123 if IMAGE_SIZE == 1024 and BASE_SIZE == 1280:
124 return ImageSize(width=1024 * 2, height=1024 * 2)
125 return ImageSize(width=768 * 2, height=768 * 2)
126
127
128 class DeepseekOCR2DummyInputsBuilder(
129 BaseDummyInputsBuilder[DeepseekOCR2ProcessingInfo]
130 ):
131 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
132 num_images = mm_counts.get("image", 0)
133
134 processor = self.info.get_hf_processor()
135 image_token = processor.image_token
136
137 return image_token * num_images
138
139 def get_dummy_mm_data(
140 self,
141 seq_len: int,
142 mm_counts: Mapping[str, int],
143 mm_options: MultiModalDummyOptions,
144 ) -> MultiModalDataDict:
145 max_image_size = self.info.get_image_size_with_most_features()
146
147 return {
148 "image": self._get_dummy_images(
149 width=max_image_size.width,
150 height=max_image_size.height,
151 num_images=mm_counts.get("image", 0),
152 )
153 }
154
155
156 class DeepseekOCR2MultiModalProcessor(
157 BaseMultiModalProcessor[DeepseekOCR2ProcessingInfo]
158 ):
159 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
160 return self.dummy_inputs.get_dummy_text(mm_counts)
161
162 def _get_hf_mm_inputs(
163 self,
164 mm_items: MultiModalDataItems,
165 hf_kwargs: Mapping[str, object],
166 ) -> HFMultiModalInputs:
167 hf_inputs = super()._get_hf_mm_inputs(mm_items, hf_kwargs)
168 if "text" in hf_inputs.hf_data:
169 hf_inputs.hf_data["prompt"] = hf_inputs.hf_data.pop("text")
170
171 return hf_inputs
172
173 def _get_mm_fields_config(
174 self,
175 hf_inputs: BatchFeature,
176 hf_processor_mm_kwargs: Mapping[str, object],
177 ) -> Mapping[str, MultiModalFieldConfig]:
178 images_spatial_crop = hf_inputs.get("images_spatial_crop", torch.empty((0, 2)))
179 is_tiled = (images_spatial_crop[:, 0] > 1) | (images_spatial_crop[:, 1] > 1)
180 patches_per_image = torch.where(is_tiled, images_spatial_crop.prod(dim=-1), 0)
181 return dict(
182 pixel_values=MultiModalFieldConfig.batched("image"),
183 images_spatial_crop=MultiModalFieldConfig.batched(
184 "image", keep_on_cpu=True
185 ),
186 images_crop=MultiModalFieldConfig.flat_from_sizes(
187 "image", patches_per_image
188 ),
189 )
190
191 def _get_prompt_updates(
192 self,
193 mm_items: MultiModalDataItems,
194 hf_processor_mm_kwargs: Mapping[str, object],
195 out_mm_kwargs: MultiModalKwargsItems,
196 ) -> Sequence[PromptUpdate]:
197 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
198
199 image_token_id = hf_processor.image_token_id
200 assert isinstance(image_token_id, int)
201
202 def get_replacement_deepseek_vl2(item_idx: int):
203 images = mm_items.get_items(
204 "image", (ImageEmbeddingItems, ImageProcessorItems)
205 )
206
207 if isinstance(images, ImageEmbeddingItems):
208 num_image_tokens = images.get_feature_size(item_idx)
209 else:
210 assert isinstance(images, ImageProcessorItems)
211 size = images.get_image_size(item_idx)
212
213 num_image_tokens = self.info.get_num_image_tokens(
214 image_width=size.width,
215 image_height=size.height,
216 cropping=CROP_MODE,
217 )
218 return [image_token_id] * num_image_tokens
219
220 return [
221 PromptReplacement(
222 modality="image",
223 target=[image_token_id],
224 replacement=get_replacement_deepseek_vl2,
225 )
226 ]
227
228
229 @MULTIMODAL_REGISTRY.register_processor(
230 DeepseekOCR2MultiModalProcessor,
231 info=DeepseekOCR2ProcessingInfo,
232 dummy_inputs=DeepseekOCR2DummyInputsBuilder,
233 )
234 class DeepseekOCR2ForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
235 hf_to_vllm_mapper = WeightsMapper(
236 orig_to_new_prefix={
237 # map prefix for language backbone
238 "model.embed_tokens.": "language_model.model.embed_tokens.",
239 "model.layers.": "language_model.model.layers.",
240 "model.norm.": "language_model.model.norm.",
241 "lm_head.": "language_model.lm_head.",
242 # remove "model." prefix for other components
243 "model.": "",
244 }
245 )
246
247 @classmethod
248 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
249 if modality.startswith("image"):
250 return "<image>"
251
252 raise ValueError("Only image modality is supported")
253
254 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
255 super().__init__()
256
257 config: DeepseekVLV2Config = vllm_config.model_config.hf_config
258 multimodal_config = vllm_config.model_config.multimodal_config
259
260 self.config = config
261 self.multimodal_config = multimodal_config
262
263 self.vision_config = config.vision_config
264 self.projector_config = config.projector_config
265 self.text_config = config.text_config
266 model_config = vllm_config.model_config
267 tokenizer = cached_tokenizer_from_config(model_config)
268 self.image_token_id = tokenizer.vocab[_IMAGE_TOKEN]
269
270 with self._mark_tower_model(vllm_config, "image"):
271 self.sam_model = ImageEncoderViT(
272 depth=12,
273 embed_dim=768,
274 img_size=1024,
275 mlp_ratio=4,
276 norm_layer=partial(torch.nn.LayerNorm, eps=1e-6),
277 num_heads=12,
278 patch_size=16,
279 qkv_bias=True,
280 use_rel_pos=True,
281 global_attn_indexes=(2, 5, 8, 11),
282 window_size=14,
283 out_chans=256,
284 last_conv_output=896,
285 )
286 self.qwen2_model = build_qwen2_decoder_as_encoder()
287
288 self.projector = MlpProjector(self.projector_config)
289 self.tile_tag = config.tile_tag
290 self.global_view_pos = config.global_view_pos
291
292 # special token for image token sequence format
293 n_embed = self.projector_config.n_embed
294 embed_std = 1 / torch.sqrt(torch.tensor(n_embed, dtype=torch.float32))
295 if self.tile_tag == "2D":
296 # This is a typo in original implementation
297 self.view_seperator = nn.Parameter(torch.randn(n_embed) * embed_std)
298 else:
299 raise ValueError(
300 f"Only 2D tile_tag is supported currently, got: {self.tile_tag}"
301 )
302
303 with self._mark_language_model(vllm_config):
304 self.language_model = init_vllm_registered_model(
305 vllm_config=vllm_config,
306 hf_config=self.text_config,
307 prefix=maybe_prefix(prefix, "language_model"),
308 )
309
310 self.make_empty_intermediate_tensors = (
311 self.language_model.make_empty_intermediate_tensors
312 )
313
314 def _parse_and_validate_image_input(
315 self, **kwargs: object
316 ) -> DeepseekOCRImagePixelInputs | None:
317 pixel_values = kwargs.pop("pixel_values", None)
318 images_spatial_crop = kwargs.pop("images_spatial_crop", None)
319 images_crop = kwargs.pop("images_crop", None)
320
321 if pixel_values is None:
322 return None
323
324 base_size = self.vision_config.image_size
325 return DeepseekOCRImagePixelInputs(
326 type="pixel_values",
327 data=pixel_values,
328 images_crop=images_crop,
329 images_spatial_crop=images_spatial_crop,
330 resolve_bindings={
331 "base_size": base_size,
332 },
333 )
334
335 def _encode_global_features(self, image_tensor: torch.Tensor) -> torch.Tensor:
336 global_features_1 = self.sam_model(image_tensor)
337 global_features_2 = self.qwen2_model(global_features_1)
338
339 features = self.projector(global_features_2)
340
341 _, hw, dim = features.shape
342
343 return features.view(-1, dim)
344
345 def _encode_local_features(self, patches: torch.Tensor) -> torch.Tensor | None:
346 if torch.sum(patches).item() == 0:
347 return None
348
349 local_features = self.sam_model(patches)
350 local_features = self.qwen2_model(local_features)
351
352 features = self.projector(local_features)
353
354 _, _, dim = features.shape
355
356 return features.view(-1, dim)
357
358 def _pixel_values_to_embedding(
359 self,
360 pixel_values: torch.Tensor,
361 images_crop: torch.Tensor,
362 images_spatial_crop: torch.Tensor,
363 ) -> NestedTensors:
364 images_in_this_batch = []
365
366 is_tiled = (images_spatial_crop[:, 0] > 1) | (images_spatial_crop[:, 1] > 1)
367 patches_per_image = torch.where(is_tiled, images_spatial_crop.prod(dim=-1), 0)
368 images_crop = images_crop.split(patches_per_image.tolist())
369 for jdx in range(images_spatial_crop.size(0)):
370 patches = images_crop[jdx]
371 image_ori = pixel_values[[jdx]]
372
373 global_features = self._encode_global_features(image_ori)
374 local_features = self._encode_local_features(patches)
375
376 if local_features is not None:
377 combined = torch.cat(
378 [local_features, global_features, self.view_seperator[None, :]],
379 dim=0,
380 )
381 else:
382 combined = torch.cat(
383 [global_features, self.view_seperator[None, :]], dim=0
384 )
385
386 images_in_this_batch.append(combined)
387
388 return images_in_this_batch
389
390 def _process_image_input(
391 self, image_input: DeepseekOCRImagePixelInputs
392 ) -> torch.Tensor:
393 pixel_values = image_input.data
394 images_crop = image_input.images_crop
395 images_spatial_crop = image_input.images_spatial_crop.to(dtype=torch.long)
396
397 vision_features = self._pixel_values_to_embedding(
398 pixel_values=pixel_values,
399 images_crop=images_crop,
400 images_spatial_crop=images_spatial_crop,
401 )
402
403 return vision_features
404
405 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
406 image_input = self._parse_and_validate_image_input(**kwargs)
407 if image_input is None:
408 return None
409 vision_embeddings = self._process_image_input(image_input)
410 return vision_embeddings
411
412 def forward(
413 self,
414 input_ids: torch.Tensor,
415 positions: torch.Tensor,
416 intermediate_tensors: IntermediateTensors | None = None,
417 inputs_embeds: torch.Tensor | None = None,
418 **kwargs: object,
419 ):
420 if intermediate_tensors is not None:
421 inputs_embeds = None
422
423 hidden_states = self.language_model(
424 input_ids, positions, intermediate_tensors, inputs_embeds=inputs_embeds
425 )
426
427 return hidden_states
428
429 def compute_logits(
430 self,
431 hidden_states: torch.Tensor,
432 ) -> torch.Tensor | None:
433 return self.language_model.compute_logits(hidden_states)
434
435 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
436 loader = AutoWeightsLoader(self)
437 autoloaded_weights = loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
438 return autoloaded_weights
439
440 def get_mm_mapping(self) -> MultiModelKeys:
441 """Get the module prefix in multimodal models."""
442 return MultiModelKeys.from_string_field(
443 language_model="language_model",
444 connector="projector",
445 tower_model=["sam_model", "qwen2_model"],
446 )