processor.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. """vLLM 0.25 multimodal processor for DeepSeek-V4-Flash-Vision-Exp.
  2. Images only. The checkpoint has no video encoder; GIF is a still frame via PIL.
  3. """
  4. from __future__ import annotations
  5. from collections.abc import Mapping, Sequence
  6. from typing import Any
  7. import torch
  8. from transformers.feature_extraction_utils import BatchFeature
  9. from vllm.inputs import MultiModalDataDict
  10. from vllm.multimodal import MULTIMODAL_REGISTRY
  11. from vllm.multimodal.inputs import MultiModalFieldConfig
  12. from vllm.multimodal.processing import (
  13. BaseDummyInputsBuilder,
  14. BaseMultiModalProcessor,
  15. BaseProcessingInfo,
  16. ProcessorInputs,
  17. PromptReplacement,
  18. PromptUpdate,
  19. TimingContext,
  20. )
  21. from .image_processor import (
  22. IMAGE_PAD,
  23. IMAGE_PLACEHOLDER,
  24. as_pil,
  25. build_image_block,
  26. pil_to_patches,
  27. vision_args_from_config,
  28. )
  29. def _image_token_id(tokenizer) -> int:
  30. convert = getattr(tokenizer, "convert_tokens_to_ids", None)
  31. if convert is not None:
  32. token_id = convert(IMAGE_PLACEHOLDER)
  33. if token_id is not None and token_id != getattr(tokenizer, "unk_token_id", None):
  34. return int(token_id)
  35. vocab = getattr(tokenizer, "get_vocab", lambda: {})()
  36. if IMAGE_PLACEHOLDER in vocab:
  37. return int(vocab[IMAGE_PLACEHOLDER])
  38. # Vision-Exp added-token id (vocab_size 129280; placeholder is in the tail).
  39. return 129264
  40. def _collect_images(mm_data: Mapping[str, object]) -> list[Any]:
  41. for key in ("images", "image"):
  42. value = mm_data.get(key)
  43. if value is None:
  44. continue
  45. if isinstance(value, (list, tuple)):
  46. return list(value)
  47. return [value]
  48. return []
  49. class DeepseekV4VisionExpProcessingInfo(BaseProcessingInfo):
  50. def get_supported_mm_limits(self) -> Mapping[str, int | None]:
  51. return {"image": 16}
  52. def get_hf_processor(self, **kwargs: object):
  53. raise RuntimeError(
  54. "DeepSeek-V4-Flash-Vision-Exp has no Hugging Face processor; "
  55. "the vLLM Vision-Exp processor handles images directly."
  56. )
  57. def get_max_image_tokens(self) -> int:
  58. return int(getattr(self.get_hf_config(), "vision_max_n_token", 384))
  59. class DeepseekV4VisionExpDummyInputsBuilder(
  60. BaseDummyInputsBuilder[DeepseekV4VisionExpProcessingInfo]
  61. ):
  62. def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
  63. return IMAGE_PLACEHOLDER * mm_counts.get("image", 0)
  64. def get_dummy_mm_data(
  65. self,
  66. seq_len: int,
  67. mm_counts: Mapping[str, int],
  68. mm_options: Mapping[str, Any],
  69. ) -> MultiModalDataDict:
  70. num_images = mm_counts.get("image", 0)
  71. image_overrides = mm_options.get("image")
  72. # Oversized square; official safe_resize caps at vision_max_n_token.
  73. return {
  74. "image": self._get_dummy_images(
  75. width=2048,
  76. height=2048,
  77. num_images=num_images,
  78. overrides=image_overrides,
  79. )
  80. }
  81. class DeepseekV4VisionExpMultiModalProcessor(
  82. BaseMultiModalProcessor[DeepseekV4VisionExpProcessingInfo]
  83. ):
  84. def _hf_processor_applies_updates(
  85. self,
  86. prompt_text: str,
  87. mm_items,
  88. hf_processor_mm_kwargs: Mapping[str, object],
  89. tokenization_kwargs: Mapping[str, object],
  90. ) -> bool:
  91. return False
  92. def _cached_apply_hf_processor(
  93. self,
  94. inputs: ProcessorInputs,
  95. timing_ctx: TimingContext,
  96. ) -> tuple[list[int], Any, bool]:
  97. # compress_pad depends on the expanded token position of each image,
  98. # so a content-only processor cache would reuse the wrong layout.
  99. if inputs.mm_data_items.get_count("image", strict=False) > 0:
  100. return self._apply_hf_processor(inputs, timing_ctx)
  101. return super()._cached_apply_hf_processor(inputs, timing_ctx)
  102. def _call_hf_processor(
  103. self,
  104. prompt: str,
  105. mm_data: Mapping[str, object],
  106. mm_kwargs: Mapping[str, object],
  107. tok_kwargs: Mapping[str, object],
  108. ) -> BatchFeature:
  109. tokenizer = self.info.get_tokenizer()
  110. encode = getattr(tokenizer, "encode", None)
  111. if encode is None:
  112. raise RuntimeError("Vision-Exp processor requires tokenizer.encode")
  113. prompt_ids = list(encode(prompt, add_special_tokens=False))
  114. images = _collect_images(mm_data)
  115. if not images:
  116. return BatchFeature(
  117. data={"input_ids": torch.tensor([prompt_ids], dtype=torch.long)}
  118. )
  119. args = vision_args_from_config(self.info.get_hf_config())
  120. image_token_id = _image_token_id(tokenizer)
  121. max_tokens = args.vision_max_n_token
  122. pixel_rows: list[torch.Tensor] = []
  123. n_vit_h_rows: list[int] = []
  124. n_vit_w_rows: list[int] = []
  125. types_rows: list[torch.Tensor] = []
  126. perm_rows: list[torch.Tensor] = []
  127. num_token_rows: list[int] = []
  128. max_patches = 1
  129. expanded_len = 0
  130. image_iter = iter(images)
  131. for tok in prompt_ids:
  132. if tok != image_token_id:
  133. expanded_len += 1
  134. continue
  135. pil = as_pil(next(image_iter))
  136. patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w = pil_to_patches(pil, args)
  137. types, perm = build_image_block(n_llm_h, n_llm_w, expanded_len)
  138. if types.numel() > max_tokens:
  139. raise ValueError(
  140. f"Image token block length {types.numel()} exceeds "
  141. f"vision_max_n_token={max_tokens}"
  142. )
  143. pixel_rows.append(patches)
  144. n_vit_h_rows.append(int(n_vit_h))
  145. n_vit_w_rows.append(int(n_vit_w))
  146. types_rows.append(types)
  147. perm_rows.append(perm)
  148. num_token_rows.append(int(types.numel()))
  149. max_patches = max(max_patches, int(patches.shape[0]))
  150. expanded_len += int(types.numel())
  151. leftover = list(image_iter)
  152. if leftover:
  153. raise ValueError(
  154. f"Found {len(images)} images but only "
  155. f"{len(images) - len(leftover)} {IMAGE_PLACEHOLDER!r} placeholders"
  156. )
  157. if len(pixel_rows) != prompt_ids.count(image_token_id):
  158. raise ValueError(
  159. f"Found {prompt_ids.count(image_token_id)} image placeholders "
  160. f"but processed {len(pixel_rows)} images"
  161. )
  162. padded_pixels = []
  163. for patches in pixel_rows:
  164. if patches.shape[0] < max_patches:
  165. pad = patches.new_zeros(
  166. max_patches - patches.shape[0], *patches.shape[1:]
  167. )
  168. patches = torch.cat([patches, pad], dim=0)
  169. padded_pixels.append(patches.to(torch.bfloat16))
  170. def _pad_1d(rows: list[torch.Tensor], fill: int, width: int) -> torch.Tensor:
  171. out = []
  172. for row in rows:
  173. if row.numel() < width:
  174. row = torch.cat(
  175. [
  176. row,
  177. torch.full((width - row.numel(),), fill, dtype=row.dtype),
  178. ]
  179. )
  180. out.append(row[:width])
  181. return torch.stack(out, dim=0)
  182. return BatchFeature(
  183. data={
  184. "input_ids": torch.tensor([prompt_ids], dtype=torch.long),
  185. "pixel_values": torch.stack(padded_pixels, dim=0),
  186. "n_vit_h": torch.tensor(n_vit_h_rows, dtype=torch.int32),
  187. "n_vit_w": torch.tensor(n_vit_w_rows, dtype=torch.int32),
  188. "types": _pad_1d(types_rows, IMAGE_PAD, max_tokens),
  189. "perm": _pad_1d(perm_rows, -1, max_tokens),
  190. "num_tokens": torch.tensor(num_token_rows, dtype=torch.int32),
  191. }
  192. )
  193. def _get_mm_fields_config(
  194. self,
  195. hf_inputs: BatchFeature,
  196. hf_processor_mm_kwargs: Mapping[str, object],
  197. ) -> Mapping[str, MultiModalFieldConfig]:
  198. return dict(
  199. pixel_values=MultiModalFieldConfig.batched("image"),
  200. n_vit_h=MultiModalFieldConfig.batched("image"),
  201. n_vit_w=MultiModalFieldConfig.batched("image"),
  202. types=MultiModalFieldConfig.batched("image"),
  203. perm=MultiModalFieldConfig.batched("image"),
  204. num_tokens=MultiModalFieldConfig.batched("image"),
  205. )
  206. def _get_prompt_updates(
  207. self,
  208. mm_items,
  209. hf_processor_mm_kwargs: Mapping[str, object],
  210. out_mm_kwargs,
  211. ) -> Sequence[PromptUpdate]:
  212. image_token_id = _image_token_id(self.info.get_tokenizer())
  213. def get_replacement(item_idx: int):
  214. item = out_mm_kwargs["image"][item_idx]
  215. raw = item["num_tokens"]
  216. num_tokens = raw.data if hasattr(raw, "data") else raw
  217. if hasattr(num_tokens, "reshape"):
  218. num_tokens = int(num_tokens.reshape(-1)[0].item())
  219. else:
  220. num_tokens = int(num_tokens)
  221. if num_tokens <= 0:
  222. raise ValueError(f"Image {item_idx} produced 0 vision tokens")
  223. return [image_token_id] * num_tokens
  224. return [
  225. PromptReplacement(
  226. modality="image",
  227. target=[image_token_id],
  228. replacement=get_replacement,
  229. )
  230. ]
  231. def register_vision_exp_processor(model_cls):
  232. return MULTIMODAL_REGISTRY.register_processor(
  233. DeepseekV4VisionExpMultiModalProcessor,
  234. info=DeepseekV4VisionExpProcessingInfo,
  235. dummy_inputs=DeepseekV4VisionExpDummyInputsBuilder,
  236. )(model_cls)