processor.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. """OpenAI-multimodal processor wiring for DeepSeek-V4-Flash-Vision-Exp.
  2. dots3_note-style processor trio (workflow of the b12x vLLM fork):
  3. - a small HF-like processor that tokenizes prompt text (placeholders intact)
  4. and precomputes ViT patches / grid / sentinel layout per image,
  5. - a dummy builder for MM profiling,
  6. - a `BaseMultiModalProcessor` whose prompt update expands every
  7. `<|deepseek_image|>` token (vocab id 129264) into the per-image sentinel
  8. block built by `image_processor.build_image_block`, with the whole block
  9. flagged as mm embeddings.
  10. `model.embed_multimodal` turns the tensors into per-image embedding runs and
  11. `model.embed_input_ids` scatters them over the flagged range on prefill.
  12. """
  13. from __future__ import annotations
  14. import math
  15. from collections.abc import Mapping, Sequence
  16. from functools import cached_property
  17. from typing import Any, cast
  18. import numpy as np
  19. import torch
  20. from PIL import Image
  21. from transformers import BatchFeature
  22. from vllm.config.multimodal import BaseDummyOptions, ImageDummyOptions
  23. from vllm.inputs import MultiModalDataDict
  24. from vllm.multimodal.inputs import MultiModalFieldConfig, MultiModalKwargsItems
  25. from vllm.multimodal.parse import MultiModalDataItems
  26. from vllm.multimodal.processing import (
  27. BaseDummyInputsBuilder,
  28. BaseMultiModalProcessor,
  29. BaseProcessingInfo,
  30. PromptReplacement,
  31. PromptUpdateDetails,
  32. )
  33. from .encoding_dsv4 import IMAGE_PLACEHOLDER
  34. from .image_processor import (
  35. COMPRESS_PAD_TO,
  36. build_image_block,
  37. grid_tokens,
  38. safe_resize,
  39. solve_resize_ratio,
  40. )
  41. _PROTO_PATCHES = 60 # prototype patch-grid side for the dummy-size sweep
  42. def _vision_kv(hf: Any) -> dict[str, int | float]:
  43. return {
  44. "vision_patch_size": int(getattr(hf, "vision_patch_size", 14)),
  45. "vision_downsample_ratio": int(getattr(hf, "vision_downsample_ratio", 3)),
  46. "vision_max_n_token": int(getattr(hf, "vision_max_n_token", 384)),
  47. "vision_min_pixels": int(getattr(hf, "vision_min_pixels", 147456)),
  48. "vision_max_wh_ratio": int(getattr(hf, "vision_max_wh_ratio", 8)),
  49. }
  50. class DSV4VisionImagePreparer:
  51. """Resize/pad/patch a PIL image per the day-0 `load_image` math."""
  52. def __init__(self, hf: Any) -> None:
  53. for key, value in _vision_kv(hf).items():
  54. setattr(self, key, value)
  55. self.image_token_id: int | None = None
  56. @property
  57. def p(self) -> int:
  58. return self.vision_patch_size
  59. def prepare(self, image: Image.Image) -> dict[str, torch.Tensor]:
  60. image = image.convert("RGB")
  61. width, height = image.size
  62. if self.vision_max_wh_ratio and width > height * self.vision_max_wh_ratio:
  63. width = height * self.vision_max_wh_ratio
  64. if 0 < width * height < self.vision_min_pixels:
  65. scale = (self.vision_min_pixels / (width * height)) ** 0.5
  66. width = int(width * scale)
  67. height = int(height * scale)
  68. best_width = math.ceil(width / self.p) * self.p
  69. best_height = math.ceil(height / self.p) * self.p
  70. n_llm_h, n_llm_w, best_height, best_width = safe_resize(
  71. height,
  72. width,
  73. best_height,
  74. best_width,
  75. self.p,
  76. self.vision_downsample_ratio,
  77. self.vision_max_n_token,
  78. )
  79. n_vit_h, n_vit_w = best_height // self.p, best_width // self.p
  80. if self.vision_max_wh_ratio and image.width >= self.vision_max_wh_ratio * image.height:
  81. resized = image.resize((best_width, best_height))
  82. else:
  83. resized = _pad_image(image, best_width, best_height)
  84. x = torch.from_numpy(np.asarray(resized, dtype="float32")).permute(2, 0, 1) / 255
  85. x = ((x - 0.5) / 0.5).to(torch.bfloat16)
  86. patches = (
  87. x.reshape(3, n_vit_h, self.p, n_vit_w, self.p)
  88. .permute(1, 3, 0, 2, 4)
  89. .reshape(n_vit_h * n_vit_w, 3, self.p, self.p)
  90. )
  91. return dict(
  92. patches=patches,
  93. n_vit_h=n_vit_h,
  94. n_vit_w=n_vit_w,
  95. n_llm_h=n_llm_h,
  96. n_llm_w=n_llm_w,
  97. )
  98. def max_block_scan(self) -> tuple[int, int, int]:
  99. """(best_h, best_w) and its block length over candidate aspect ratios."""
  100. base = _PROTO_PATCHES * self.p * self.vision_downsample_ratio
  101. ratios = [1 / r for r in (1, 2, 3, 4, 5, 6, 7, 8)]
  102. ratios += [1 / r for r in ratios[1:]]
  103. best: tuple[int, int, int] = (0, 0, 0)
  104. for height_ratio in ratios:
  105. height = max(self.p * self.vision_downsample_ratio, int(base * height_ratio))
  106. width = max(self.p * self.vision_downsample_ratio, int(base / height_ratio))
  107. best_h, best_w = math.ceil(height / self.p) * self.p, math.ceil(width / self.p) * self.p
  108. n_h, n_w, fb_h, fb_w, _ = solve_resize_ratio(
  109. height, width, self.p, self.vision_downsample_ratio, self.vision_max_n_token
  110. )
  111. best_h, best_w = max(best_h, fb_h), max(best_w, fb_w)
  112. _, _, num = grid_tokens(fb_h, fb_w, self.p, self.vision_downsample_ratio)
  113. if num > best[2]:
  114. best = (fb_h, fb_w, num + COMPRESS_PAD_TO - 1)
  115. return best
  116. def _pad_image(image: Image.Image, width: int, height: int) -> Image.Image:
  117. from PIL import ImageOps
  118. return ImageOps.pad(image, (width, height), color=(127, 127, 127))
  119. class DSV4VisionProcessingInfo(BaseProcessingInfo):
  120. @cached_property
  121. def preparer(self) -> DSV4VisionImagePreparer:
  122. return DSV4VisionImagePreparer(self.get_hf_config())
  123. @cached_property
  124. def processor(self) -> "DSV4VisionProcessor":
  125. return DSV4VisionProcessor(
  126. self.get_tokenizer(),
  127. self.preparer,
  128. vocab_size=int(self.get_hf_config().vocab_size),
  129. )
  130. def get_hf_processor(self, **kwargs: object) -> "DSV4VisionProcessor":
  131. return self.processor
  132. def get_supported_mm_limits(self) -> Mapping[str, int | None]:
  133. return {"image": None}
  134. @cached_property
  135. def max_image_size(self) -> tuple[int, int]:
  136. best_h, best_w, _ = self.preparer.max_block_scan()
  137. return best_w, best_h
  138. def get_mm_max_tokens_per_item(
  139. self,
  140. seq_len: int,
  141. mm_counts: Mapping[str, int],
  142. ) -> Mapping[str, int]:
  143. if not mm_counts.get("image"):
  144. return {}
  145. _, _, num = self.preparer.max_block_scan()
  146. return {"image": num + 4}
  147. class DSV4VisionProcessor:
  148. """HF-like processor: tokenize text, precompute image tensors."""
  149. def __init__(self, tokenizer, preparer: DSV4VisionImagePreparer, vocab_size: int) -> None:
  150. self.tokenizer = tokenizer
  151. self.preparer = preparer
  152. self.vocab_size = vocab_size
  153. ids = tokenizer.convert_tokens_to_ids(IMAGE_PLACEHOLDER)
  154. if ids is None or ids == getattr(tokenizer, "unk_token_id", -1):
  155. raise ValueError(f"Token not found in tokenizer: {IMAGE_PLACEHOLDER}")
  156. self.image_token_id = int(ids)
  157. preparer.image_token_id = self.image_token_id
  158. def _encode(self, text: str) -> list[int]:
  159. try:
  160. ids = self.tokenizer.encode(text, add_special_tokens=False)
  161. except TypeError:
  162. ids = self.tokenizer.encode(text)
  163. if ids and isinstance(ids[0], list):
  164. ids = ids[0]
  165. return list(cast("list[int]", ids))
  166. def __call__(self, text: str, images: Sequence[Image.Image] | None = None, **_kw: Any) -> BatchFeature:
  167. image_list = list(images or [])
  168. token_ids = self._encode(text)
  169. found = token_ids.count(self.image_token_id)
  170. if found < len(image_list):
  171. extra = " ".join([IMAGE_PLACEHOLDER] * (len(image_list) - found))
  172. text = f"{text}\n{extra}" if found else text + extra
  173. token_ids = self._encode(text)
  174. found = token_ids.count(self.image_token_id)
  175. if found != len(image_list):
  176. raise ValueError(
  177. f"Found {found} image tokens but got {len(image_list)} images"
  178. )
  179. patches: list[torch.Tensor] = []
  180. grids: list[torch.Tensor] = []
  181. types: list[torch.Tensor] = []
  182. perms: list[torch.Tensor] = []
  183. perm_counts: list[int] = []
  184. running = 0
  185. image_idx = 0
  186. for pos, tok in enumerate(token_ids):
  187. if tok != self.image_token_id:
  188. running += 1
  189. continue
  190. prep = self.preparer.prepare(image_list[image_idx])
  191. n_vit_h, n_vit_w, n_llm_h, n_llm_w = (
  192. prep["n_vit_h"],
  193. prep["n_vit_w"],
  194. prep["n_llm_h"],
  195. prep["n_llm_w"],
  196. )
  197. blk_types, perm = build_image_block(n_llm_h, n_llm_w, running)
  198. patches.append(prep["patches"])
  199. grids.append(
  200. torch.tensor([n_vit_h, n_vit_w, n_llm_h, n_llm_w], dtype=torch.long)
  201. )
  202. types.append(blk_types)
  203. perms.append(perm)
  204. perm_counts.append(int(perm.numel()))
  205. image_idx += 1
  206. running += int(blk_types.numel())
  207. data: dict[str, Any] = {"input_ids": [token_ids]}
  208. if patches:
  209. data["image_patches"] = torch.cat(patches)
  210. data["image_grids"] = torch.stack(grids)
  211. data["image_types"] = torch.cat(types)
  212. data["image_perm"] = torch.cat(perms)
  213. data["image_patch_counts"] = torch.tensor(
  214. [int(pt.shape[0]) for pt in patches], dtype=torch.long
  215. )
  216. data["image_type_counts"] = torch.tensor(
  217. [int(t.numel()) for t in types], dtype=torch.long
  218. )
  219. data["image_perm_counts"] = torch.tensor(perm_counts, dtype=torch.long)
  220. return BatchFeature(data=data)
  221. class DSV4VisionDummyInputsBuilder(BaseDummyInputsBuilder[DSV4VisionProcessingInfo]):
  222. def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
  223. return IMAGE_PLACEHOLDER * mm_counts.get("image", 0)
  224. def get_dummy_mm_data(
  225. self,
  226. seq_len: int,
  227. mm_counts: Mapping[str, int],
  228. mm_options: Mapping[str, BaseDummyOptions],
  229. ) -> MultiModalDataDict:
  230. num = mm_counts.get("image", 0)
  231. if not num:
  232. return {}
  233. width, height = self.info.max_image_size
  234. overrides = cast(ImageDummyOptions | None, mm_options.get("image"))
  235. if overrides is not None and overrides.width and overrides.height:
  236. width, height = overrides.width, overrides.height
  237. image = Image.new("RGB", (width, height), color=(127, 127, 127))
  238. return {"image": [image] * num}
  239. class DSV4VisionMultiModalProcessor(BaseMultiModalProcessor[DSV4VisionProcessingInfo]):
  240. def _call_hf_processor(
  241. self,
  242. prompt: str,
  243. mm_data: Mapping[str, object],
  244. mm_kwargs: Mapping[str, object],
  245. tok_kwargs: Mapping[str, object],
  246. ) -> BatchFeature:
  247. return self.info.ctx.call_hf_processor(
  248. self.info.get_hf_processor(**mm_kwargs),
  249. dict(text=prompt, **mm_data),
  250. dict(**mm_kwargs, **tok_kwargs),
  251. )
  252. def _hf_processor_applies_updates(
  253. self,
  254. prompt_text: str,
  255. mm_items: MultiModalDataItems,
  256. hf_processor_mm_kwargs: Mapping[str, object],
  257. tokenization_kwargs: Mapping[str, object],
  258. ) -> bool:
  259. return False
  260. def _get_mm_fields_config(
  261. self,
  262. hf_inputs: BatchFeature,
  263. hf_processor_mm_kwargs: Mapping[str, object],
  264. ) -> Mapping[str, MultiModalFieldConfig]:
  265. if "image_patches" not in hf_inputs:
  266. return {}
  267. patch_counts = hf_inputs["image_patch_counts"]
  268. type_counts = hf_inputs["image_type_counts"]
  269. perm_counts = hf_inputs["image_perm_counts"]
  270. ones = torch.ones(len(patch_counts), dtype=torch.long)
  271. fields: dict[str, MultiModalFieldConfig] = {
  272. "image_patches": MultiModalFieldConfig.flat_from_sizes(
  273. "image", patch_counts
  274. ),
  275. "image_perm": MultiModalFieldConfig.flat_from_sizes(
  276. "image", perm_counts
  277. ),
  278. "image_types": MultiModalFieldConfig.flat_from_sizes(
  279. "image", type_counts
  280. ),
  281. }
  282. for key in (
  283. "image_grids",
  284. "image_patch_counts",
  285. "image_type_counts",
  286. "image_perm_counts",
  287. ):
  288. fields[key] = MultiModalFieldConfig.flat_from_sizes("image", ones)
  289. return fields
  290. def _get_prompt_updates(
  291. self,
  292. mm_items: MultiModalDataItems,
  293. hf_processor_mm_kwargs: Mapping[str, object],
  294. out_mm_kwargs: MultiModalKwargsItems,
  295. ) -> Sequence:
  296. token_id = self.info.processor.image_token_id
  297. def replacement(item_idx: int) -> PromptUpdateDetails[list[int]]:
  298. count = int(out_mm_kwargs["image"][item_idx]["image_type_counts"].data)
  299. return PromptUpdateDetails.select_token_id(
  300. [token_id] * count, token_id
  301. )
  302. return [
  303. PromptReplacement(
  304. modality="image",
  305. target=token_id,
  306. replacement=replacement,
  307. )
  308. ]