image_processor.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. """Official DeepSeek-V4-Flash-Vision-Exp image preprocessor.
  2. Adapted from deepseek-ai/DeepSeek-V4-Flash-Vision-Exp ``inference/image_processor.py``
  3. (MIT). Adds a PIL entry point so vLLM can feed decoded images without going
  4. back through URLs. Video is not part of this checkpoint: GIF is decoded as a
  5. still RGB frame (PIL's first frame).
  6. """
  7. from __future__ import annotations
  8. import base64
  9. import io
  10. import math
  11. from dataclasses import dataclass
  12. from types import SimpleNamespace
  13. from typing import Any, TYPE_CHECKING
  14. from urllib.request import urlopen
  15. if TYPE_CHECKING:
  16. import torch
  17. from PIL import Image as PILImage
  18. def _pil():
  19. """Load Pillow on first image decode. CPU CI does not install it."""
  20. try:
  21. from PIL import Image, ImageOps
  22. except ImportError as exc:
  23. raise ModuleNotFoundError(
  24. "Pillow is required to decode Vision-Exp images"
  25. ) from exc
  26. return Image, ImageOps
  27. IMAGE_START, IMAGE_PAD, IMAGE, IMAGE_NEW_LINE, IMAGE_END = range(5)
  28. COMPRESS_PAD_TO = 4
  29. IMAGE_PLACEHOLDER = "<|deepseek_image|>"
  30. def is_vision_exp_weight_name(name: str) -> bool:
  31. """Checkpoint keys for the ViT/Aligner/image tokens.
  32. Anemll ``load_weights`` treats any name containing ``w1`` as a stacked
  33. ``gate_up_proj`` shard. Aligner's ``w1`` and ViT MLP ``w1`` are full
  34. Linear layers, so those keys must bypass the stacked mapping.
  35. """
  36. return (
  37. name.startswith("aligner.")
  38. or name.startswith("vision.")
  39. or name.startswith("image_")
  40. or name.startswith("model.aligner.")
  41. or name.startswith("model.vision.")
  42. or name.startswith("model.image_")
  43. )
  44. def is_unregistered_router_bias(name: str, param_names: Any) -> bool:
  45. """True when a gate bias has no Parameter.
  46. Hash MoE (layers 0–2) does not register ``e_score_correction_bias``.
  47. 0731 omitted those tensors; Vision-Exp dumps ``ffn.gate.bias`` /
  48. ``bias_vl`` there anyway. Anemll's mapper rewrites them to
  49. ``e_score_correction_bias`` / ``_vl``, then ``load_weights`` KeyErrors.
  50. The DSpark draft loader uses ``endswith(".ffn.gate.bias")``, which
  51. misses ``bias_vl``, so unmapped ``.ffn.gate.bias_vl`` is also unused
  52. until that remap is patched.
  53. """
  54. if name in param_names:
  55. return False
  56. return name.endswith(
  57. (
  58. ".ffn.gate.e_score_correction_bias",
  59. ".ffn.gate.e_score_correction_bias_vl",
  60. ".ffn.gate.bias_vl",
  61. ".ffn.gate.bias",
  62. )
  63. )
  64. def as_pil(item: Any) -> PILImage.Image:
  65. """Normalize vLLM image items (PIL, HWC/CHW array, tensor, dict) to RGB PIL."""
  66. Image, _ImageOps = _pil()
  67. if isinstance(item, Image.Image):
  68. return item.convert("RGB")
  69. if isinstance(item, dict):
  70. for key in ("image_pil", "pil", "image"):
  71. value = item.get(key)
  72. if value is None or isinstance(value, dict):
  73. continue
  74. return as_pil(value)
  75. raise TypeError(f"Unsupported image dict keys: {sorted(item)!r}")
  76. array = item
  77. try:
  78. import torch
  79. except ImportError:
  80. torch = None # type: ignore[assignment]
  81. if torch is not None and isinstance(array, torch.Tensor):
  82. array = array.detach().cpu().numpy()
  83. try:
  84. import numpy as np
  85. except ImportError as exc:
  86. raise TypeError(f"Unsupported image item type: {type(item)!r}") from exc
  87. if not hasattr(array, "ndim"):
  88. raise TypeError(f"Unsupported image item type: {type(item)!r}")
  89. arr = np.asarray(array)
  90. if arr.ndim == 2:
  91. arr = np.stack([arr, arr, arr], axis=-1)
  92. elif arr.ndim == 3 and arr.shape[-1] not in (1, 3, 4) and arr.shape[0] in (1, 3, 4):
  93. arr = np.transpose(arr, (1, 2, 0))
  94. if arr.ndim != 3:
  95. raise TypeError(f"Unsupported image array shape: {arr.shape!r}")
  96. if arr.shape[-1] == 1:
  97. arr = np.repeat(arr, 3, axis=-1)
  98. if np.issubdtype(arr.dtype, np.floating):
  99. peak = float(arr.max()) if arr.size else 0.0
  100. if peak <= 1.0:
  101. arr = arr * 255.0
  102. arr = np.clip(arr, 0, 255).astype("uint8")
  103. else:
  104. arr = np.clip(arr, 0, 255).astype("uint8")
  105. if arr.shape[-1] == 4:
  106. return Image.fromarray(arr, mode="RGBA").convert("RGB")
  107. return Image.fromarray(arr[..., :3], mode="RGB")
  108. @dataclass
  109. class ImageInput:
  110. start: int
  111. patches: Any
  112. n_vit_h: int
  113. n_vit_w: int
  114. types: Any
  115. perm: Any
  116. def vision_args_from_config(config: Any) -> SimpleNamespace:
  117. return SimpleNamespace(
  118. vision_patch_size=int(getattr(config, "vision_patch_size", 14)),
  119. vision_downsample_ratio=int(getattr(config, "vision_downsample_ratio", 3)),
  120. vision_max_n_token=int(getattr(config, "vision_max_n_token", 384)),
  121. vision_min_pixels=int(getattr(config, "vision_min_pixels", 147456)),
  122. vision_max_wh_ratio=getattr(config, "vision_max_wh_ratio", 8),
  123. vision_n_layers=int(getattr(config, "vision_n_layers", 0)),
  124. vision_dim=int(getattr(config, "vision_dim", 1024)),
  125. vision_n_heads=int(getattr(config, "vision_n_heads", 16)),
  126. vision_inter_dim=int(getattr(config, "vision_inter_dim", 2816)),
  127. vision_rope_theta=float(getattr(config, "vision_rope_theta", 10000.0)),
  128. vocab_size=int(getattr(config, "vocab_size", 129280)),
  129. dim=int(getattr(config, "hidden_size", getattr(config, "dim", 4096))),
  130. hidden_size=int(getattr(config, "hidden_size", getattr(config, "dim", 4096))),
  131. )
  132. def grid_tokens(best_height, best_width, patch_size, downsample_ratio):
  133. """Number of LLM tokens the aligner grid occupies (N-layout, incl. row/align padding)."""
  134. n_llm_h = math.ceil((best_height // patch_size) / downsample_ratio)
  135. n_llm_w = math.ceil((best_width // patch_size) / downsample_ratio)
  136. num_tokens = n_llm_h * (n_llm_w + 1) + 2
  137. if n_llm_h % 2 == 1:
  138. num_tokens += n_llm_w + 1
  139. num_tokens += (n_llm_h + 1) // 2 * (n_llm_w + 1) % 2 * 2
  140. return n_llm_h, n_llm_w, num_tokens
  141. def solve_resize_ratio(height, width, patch_size, downsample_ratio, max_n_token):
  142. r = height / width
  143. max_w_float = math.sqrt((max_n_token - 2) / r + 0.25) - 0.5
  144. max_h_float = max_w_float * r
  145. if max_w_float < 1.0:
  146. max_w = 1
  147. max_h = (max_n_token - 2) // (max_w + 1)
  148. if max_h % 2 == 1:
  149. max_h -= 1
  150. best_width = max_w * patch_size * downsample_ratio
  151. best_height = max_h * patch_size * downsample_ratio
  152. elif max_h_float < 2.0:
  153. max_h = 2
  154. max_w = ((max_n_token - 2) // max_h) - 1
  155. assert max_w > 1
  156. best_width = max_w * patch_size * downsample_ratio
  157. best_height = max_h * patch_size * downsample_ratio
  158. else:
  159. max_w = math.floor(max_w_float)
  160. max_h = math.floor(max_h_float)
  161. if max_h % 2 == 1:
  162. max_h -= 1
  163. beta = min(
  164. max_w * patch_size * downsample_ratio / width,
  165. max_h * patch_size * downsample_ratio / height,
  166. )
  167. best_width = math.floor(width * beta / patch_size) * patch_size
  168. best_height = math.floor(height * beta / patch_size) * patch_size
  169. n_llm_h, n_llm_w, num_tokens = grid_tokens(
  170. best_height, best_width, patch_size, downsample_ratio
  171. )
  172. return n_llm_h, n_llm_w, best_height, best_width, num_tokens
  173. def safe_resize(height, width, best_height, best_width, patch_size, downsample_ratio, max_n_token):
  174. max_n_token -= COMPRESS_PAD_TO - 1
  175. n_llm_h, n_llm_w, num_tokens = grid_tokens(
  176. best_height, best_width, patch_size, downsample_ratio
  177. )
  178. budget = max_n_token
  179. while num_tokens > max_n_token:
  180. n_llm_h, n_llm_w, best_height, best_width, num_tokens = solve_resize_ratio(
  181. height, width, patch_size, downsample_ratio, budget
  182. )
  183. budget -= 1
  184. return n_llm_h, n_llm_w, best_height, best_width
  185. def load_image_bytes(record) -> bytes:
  186. """Load image bytes from raw/base64 data, an Anthropic source, URL, or path."""
  187. data = record.get("data")
  188. if isinstance(data, bytes):
  189. return data
  190. if isinstance(data, str):
  191. return base64.b64decode(data)
  192. source = record.get("source")
  193. if isinstance(source, dict):
  194. if source.get("data") is not None:
  195. return base64.b64decode(source["data"])
  196. if source.get("url"):
  197. return load_image_bytes({"url": source["url"]})
  198. url = record.get("url")
  199. if isinstance(url, str) and url:
  200. if url.startswith("data:"):
  201. header, _, payload = url.partition(",")
  202. if ";base64" not in header:
  203. raise ValueError(f"Unsupported data URL encoding: {header}")
  204. return base64.b64decode(payload)
  205. if url.startswith(("http://", "https://")):
  206. with urlopen(url, timeout=30) as response:
  207. return response.read()
  208. with open(url, "rb") as file:
  209. return file.read()
  210. raise ValueError(f"Cannot load image from record: {list(record.keys())}")
  211. def pil_to_patches(image: PILImage.Image, args) -> tuple[Any, int, int, int, int]:
  212. """Resize/pad one RGB image and return ViT patches plus LLM grid sizes."""
  213. import torch
  214. _Image, ImageOps = _pil()
  215. p = args.vision_patch_size
  216. image = image.convert("RGB")
  217. width, height = image.size
  218. if args.vision_max_wh_ratio is not None and width > height * args.vision_max_wh_ratio:
  219. width = height * args.vision_max_wh_ratio
  220. if 0 < width * height < args.vision_min_pixels:
  221. ratio = (args.vision_min_pixels / (width * height)) ** 0.5
  222. width = int(width * ratio)
  223. height = int(height * ratio)
  224. best_width = math.ceil(width / p) * p
  225. best_height = math.ceil(height / p) * p
  226. n_llm_h, n_llm_w, best_height, best_width = safe_resize(
  227. height,
  228. width,
  229. best_height,
  230. best_width,
  231. p,
  232. args.vision_downsample_ratio,
  233. args.vision_max_n_token,
  234. )
  235. n_vit_h, n_vit_w = best_height // p, best_width // p
  236. src_w, src_h = image.size
  237. if args.vision_max_wh_ratio is not None and src_w >= args.vision_max_wh_ratio * src_h:
  238. image = image.resize((best_width, best_height))
  239. else:
  240. image = ImageOps.pad(image, (best_width, best_height), color=(127, 127, 127))
  241. try:
  242. import numpy as np
  243. x = torch.from_numpy(np.asarray(image, dtype=np.float32)).permute(2, 0, 1) / 255
  244. except ImportError:
  245. pixels = (
  246. image.get_flattened_data()
  247. if hasattr(image, "get_flattened_data")
  248. else image.getdata()
  249. )
  250. x = torch.tensor(pixels, dtype=torch.float32)
  251. x = x.view(image.height, image.width, 3).permute(2, 0, 1) / 255
  252. x = ((x - 0.5) / 0.5).to(torch.bfloat16)
  253. patches = (
  254. x.reshape(3, n_vit_h, p, n_vit_w, p)
  255. .permute(1, 3, 0, 2, 4)
  256. .reshape(n_vit_h * n_vit_w, 3, p, p)
  257. )
  258. return patches, n_vit_h, n_vit_w, n_llm_h, n_llm_w
  259. def load_image(record, args):
  260. """Load and transform one image record into ViT patches."""
  261. Image, _ImageOps = _pil()
  262. with Image.open(io.BytesIO(load_image_bytes(record))) as source:
  263. image = source.convert("RGB")
  264. return pil_to_patches(image, args)
  265. def build_image_block(n_llm_h: int, n_llm_w: int, start_pos: int):
  266. """Builds the N-layout token types (final order) and the aligner-row order for IMAGE slots."""
  267. import torch
  268. compress_pad = COMPRESS_PAD_TO - 1 - start_pos % COMPRESS_PAD_TO
  269. pad_h = n_llm_h % 2
  270. rows = n_llm_h + pad_h
  271. row_len = n_llm_w + 1
  272. pad_last = rows // 2 * row_len % 2 * 2
  273. types = torch.tensor(
  274. ([IMAGE] * n_llm_w + [IMAGE_NEW_LINE]) * n_llm_h + [IMAGE_PAD] * (row_len * pad_h),
  275. dtype=torch.int64,
  276. )
  277. order = torch.arange(rows * row_len).view(rows // 2, 2, row_len).transpose(1, 2).reshape(-1)
  278. image_idx = torch.full((rows * row_len,), -1, dtype=torch.int64)
  279. image_idx.view(rows, row_len)[:n_llm_h, :n_llm_w] = torch.arange(n_llm_h * n_llm_w).view(
  280. n_llm_h, n_llm_w
  281. )
  282. perm = image_idx[order]
  283. perm = perm[perm >= 0]
  284. types = torch.cat(
  285. [
  286. torch.full((compress_pad,), IMAGE_PAD, dtype=torch.int64),
  287. torch.tensor([IMAGE_START]),
  288. types[order],
  289. torch.full((pad_last,), IMAGE_PAD, dtype=torch.int64),
  290. torch.tensor([IMAGE_END]),
  291. ]
  292. )
  293. return types, perm