model.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. """Vision-Exp wrapper around vLLM DeepseekV4ForCausalLM.
  2. Loads official `vision.*` / `aligner.*` / image sentinel tensors and merges
  3. ViT embeddings on prefill. Language decode stays on the B12X DeepSeek V4 path.
  4. The OpenAI multimodal branch (processor trio + embed-merge hooks) is gated
  5. behind VLLM_DSV4_MM=1: the fork's V2 "always-embed" mm arm routes through
  6. this override with a [B, T]-ranks ids slice and currently degenerates
  7. logits, so with VLLM_DSV4_MM unset (the default) the lane boots text-only —
  8. the verified state — and VLLM_DSV4_MM=1 opts into the experimental path.
  9. """
  10. from __future__ import annotations
  11. import os
  12. from types import SimpleNamespace
  13. from typing import Any
  14. import torch
  15. import torch.nn as nn
  16. from vllm.model_executor.models.utils import AutoWeightsLoader
  17. from vllm.models.deepseek_v4 import DeepseekV4ForCausalLM
  18. from vllm.multimodal import MULTIMODAL_REGISTRY
  19. from .processor import (
  20. DSV4VisionDummyInputsBuilder,
  21. DSV4VisionMultiModalProcessor,
  22. DSV4VisionProcessingInfo,
  23. )
  24. from .vision import Aligner, ViT
  25. _MM_ON = os.environ.get("VLLM_DSV4_MM", "0") == "1"
  26. def _maybe_mm(cls):
  27. """Register the OpenAI multimodal processor trio only when mm is on."""
  28. if not _MM_ON:
  29. return cls
  30. return MULTIMODAL_REGISTRY.register_processor(
  31. DSV4VisionMultiModalProcessor,
  32. info=DSV4VisionProcessingInfo,
  33. dummy_inputs=DSV4VisionDummyInputsBuilder,
  34. )(cls)
  35. def _vision_args(hf: Any) -> SimpleNamespace:
  36. return SimpleNamespace(
  37. vision_n_layers=int(getattr(hf, "vision_n_layers", 0) or 0),
  38. vision_dim=int(getattr(hf, "vision_dim", 1024)),
  39. vision_n_heads=int(getattr(hf, "vision_n_heads", 16)),
  40. vision_inter_dim=int(getattr(hf, "vision_inter_dim", 2816)),
  41. vision_patch_size=int(getattr(hf, "vision_patch_size", 14)),
  42. vision_rope_theta=float(getattr(hf, "vision_rope_theta", 10000.0)),
  43. vision_downsample_ratio=int(getattr(hf, "vision_downsample_ratio", 3)),
  44. vision_max_n_token=int(getattr(hf, "vision_max_n_token", 384)),
  45. vision_min_pixels=int(getattr(hf, "vision_min_pixels", 147456)),
  46. vision_max_wh_ratio=int(getattr(hf, "vision_max_wh_ratio", 8)),
  47. dim=int(getattr(hf, "hidden_size", 4096)),
  48. num_hash_layers=int(getattr(hf, "num_hash_layers", 0) or 0),
  49. )
  50. @_maybe_mm
  51. class DeepseekV4VisionForCausalLM(DeepseekV4ForCausalLM):
  52. # ModelConfig.is_multimodal_model keys off this class attribute
  53. # (interfaces.supports_multimodal) — without it the OpenAI layer
  54. # answers "not a multimodal model" for image_url requests. Gated by
  55. # _maybe_mm so the text-only default lane stays verified.
  56. supports_multimodal = _MM_ON
  57. # The fork's hash MoE routes by token id (tid2eid gather), so the merged
  58. # mm path must keep input_ids alongside the merged embeddings.
  59. requires_raw_input_tokens = _MM_ON
  60. supports_multimodal_raw_input_only = False
  61. supports_encoder_tp_data = False
  62. supports_mm_device_do_normalize = False
  63. @classmethod
  64. def get_placeholder_str(cls, modality: str, i: int) -> str | None:
  65. # chat_utils asks the resolved model class for the per-image prompt
  66. # placeholder; the tokenizer encodes it to <|deepseek_image|> (id 129264).
  67. if _MM_ON and modality.startswith("image"):
  68. from .encoding_dsv4 import IMAGE_PLACEHOLDER
  69. return IMAGE_PLACEHOLDER
  70. return None
  71. def __init__(self, *, vllm_config, prefix: str = ""):
  72. super().__init__(vllm_config=vllm_config, prefix=prefix)
  73. hf = vllm_config.model_config.hf_config
  74. args = _vision_args(hf)
  75. self._vision_args = args
  76. if args.vision_n_layers <= 0:
  77. self.vision = None
  78. self.aligner = None
  79. return
  80. self.vision = ViT(args)
  81. self.aligner = Aligner(args)
  82. self.image_start = nn.Parameter(torch.empty(args.dim))
  83. self.image_end = nn.Parameter(torch.empty(args.dim))
  84. self.image_newline = nn.Parameter(torch.empty(args.dim))
  85. self.image_pad = nn.Parameter(torch.empty(args.dim))
  86. def encode_image(self, patches: torch.Tensor, n_vit_h: int, n_vit_w: int) -> torch.Tensor:
  87. return self.aligner(self.vision(patches, n_vit_h, n_vit_w), n_vit_h, n_vit_w)
  88. def embed_input_ids(
  89. self,
  90. input_ids: torch.Tensor,
  91. multimodal_embeddings=None,
  92. *,
  93. is_multimodal: torch.Tensor | None = None,
  94. ) -> torch.Tensor:
  95. # The fork's embed output follows the input rank: [B, T, H] from the
  96. # speculator's [B, T] ids, [T, H] from plain [T] ids. mm rows must
  97. # land on the same rows either way (they never exceed inner dims).
  98. if is_multimodal is not None:
  99. # Sentinel block ids may exceed vocab; zero them before lookup.
  100. input_ids = input_ids.masked_fill(
  101. is_multimodal.to(input_ids.device).unsqueeze(-1), 0
  102. )
  103. out = super().embed_input_ids(input_ids)
  104. if multimodal_embeddings:
  105. mm_stack = torch.cat(multimodal_embeddings) # [N, H]
  106. if out.dim() == 3:
  107. out[is_multimodal] = mm_stack.to(out.dtype)
  108. else:
  109. out.masked_scatter_(
  110. is_multimodal.to(out.device).unsqueeze(-1),
  111. mm_stack.reshape(-1),
  112. )
  113. return out
  114. def embed_multimodal(self, **kwargs) -> tuple:
  115. patches = kwargs.get("image_patches")
  116. if patches is None or self.vision is None:
  117. return ()
  118. grids = kwargs["image_grids"].data
  119. types_all = kwargs["image_types"].data
  120. perm_all = kwargs["image_perm"].data
  121. t_counts = kwargs["image_type_counts"].data.tolist()
  122. perm_counts = kwargs["image_perm_counts"].data.tolist()
  123. n_items = int(grids.shape[0])
  124. out: list[torch.Tensor] = []
  125. p_off = t_off = q_off = 0
  126. start = self.image_start
  127. pad = self.image_pad
  128. newline = self.image_newline
  129. end = self.image_end
  130. for i in range(n_items):
  131. n_vit_h, n_vit_w, n_llm_h, n_llm_w = grids[i].tolist()
  132. p_count = n_vit_h * n_vit_w
  133. t_count = int(t_counts[i])
  134. q_count = int(perm_counts[i])
  135. item_types = types_all[t_off : t_off + t_count].tolist()
  136. item_perm = perm_all[q_off : q_off + q_count].to(torch.long)
  137. if q_count != sum(t == 2 for t in item_types):
  138. raise ValueError(
  139. "DSV4 vision perm/sentinel layout mismatch "
  140. f"({q_count} != {sum(t == 2 for t in item_types)})"
  141. )
  142. t_off += t_count
  143. q_off += q_count
  144. vit = self.encode_image(patches[p_off : p_off + p_count], n_vit_h, n_vit_w)
  145. p_off += p_count
  146. rows: list[torch.Tensor] = []
  147. vi = 0
  148. for t in item_types:
  149. if t == 0: # IMAGE_START
  150. rows.append(start)
  151. elif t == 1: # IMAGE_PAD
  152. rows.append(pad)
  153. elif t == 2: # IMAGE
  154. rows.append(vit[int(item_perm[vi])])
  155. vi += 1
  156. elif t == 3: # IMAGE_NEW_LINE
  157. rows.append(newline)
  158. else: # IMAGE_END
  159. rows.append(end)
  160. out.append(torch.stack(rows))
  161. return tuple(out)
  162. def forward(
  163. self,
  164. input_ids,
  165. positions,
  166. intermediate_tensors=None,
  167. inputs_embeds=None,
  168. **kwargs,
  169. ):
  170. # The runner embeds via embed_input_ids (mm merge) and hands us
  171. # merged inputs_embeds; lm_head and get_mtp_target_hidden_states
  172. # stay on the parent class.
  173. if not _MM_ON and inputs_embeds is not None:
  174. # text-only belt-and-braces: without the mm gate the parent never
  175. # sees runner-generated embeds, but if one sneaks in, sync the
  176. # token path instead so hidden shapes stay native.
  177. inputs_embeds = None
  178. return super().forward(
  179. input_ids,
  180. positions,
  181. intermediate_tensors,
  182. inputs_embeds,
  183. )
  184. def load_weights(self, weights):
  185. if self.vision is None:
  186. return super().load_weights(weights)
  187. leftover = []
  188. stacked = {
  189. "vision.": self.vision,
  190. "aligner.": self.aligner,
  191. }
  192. params = {
  193. "image_start": self.image_start,
  194. "image_end": self.image_end,
  195. "image_newline": self.image_newline,
  196. "image_pad": self.image_pad,
  197. }
  198. for name, tensor in weights:
  199. if name in params:
  200. params[name].data.copy_(tensor)
  201. continue
  202. routed = False
  203. for prefix, module in stacked.items():
  204. if name.startswith(prefix):
  205. AutoWeightsLoader(module).load_weights(
  206. [(name[len(prefix) :], tensor)]
  207. )
  208. routed = True
  209. break
  210. if not routed:
  211. leftover.append((name, tensor))
  212. leftover = [
  213. pair
  214. for pair in leftover
  215. if not _skip_unmapped_gate(pair[0], self._vision_args)
  216. ]
  217. return super().load_weights(leftover)
  218. def _skip_unmapped_gate(name: str, args: SimpleNamespace) -> bool:
  219. # Hash MoE layers register tid2eid and leave e_score_correction_bias as
  220. # None. The B12X mapper still rewrites ffn.gate.bias onto that missing
  221. # name. bias_vl has no mapper at all.
  222. if name.endswith("ffn.gate.bias_vl"):
  223. return True
  224. if name.endswith("ffn.gate.bias"):
  225. parts = name.split(".")
  226. if (
  227. len(parts) >= 5
  228. and parts[0] == "layers"
  229. and parts[1].isdigit()
  230. and int(parts[1]) < args.num_hash_layers
  231. ):
  232. return True
  233. return False