model.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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. """
  5. from __future__ import annotations
  6. from types import SimpleNamespace
  7. from typing import Any
  8. import torch
  9. import torch.nn as nn
  10. import torch
  11. import torch.nn as nn
  12. from vllm.model_executor.models.utils import AutoWeightsLoader
  13. from vllm.models.deepseek_v4 import DeepseekV4ForCausalLM
  14. from vllm.multimodal import MULTIMODAL_REGISTRY
  15. from .processor import (
  16. DSV4VisionDummyInputsBuilder,
  17. DSV4VisionMultiModalProcessor,
  18. DSV4VisionProcessingInfo,
  19. )
  20. from .vision import Aligner, ViT
  21. def _vision_args(hf: Any) -> SimpleNamespace:
  22. return SimpleNamespace(
  23. vision_n_layers=int(getattr(hf, "vision_n_layers", 0) or 0),
  24. vision_dim=int(getattr(hf, "vision_dim", 1024)),
  25. vision_n_heads=int(getattr(hf, "vision_n_heads", 16)),
  26. vision_inter_dim=int(getattr(hf, "vision_inter_dim", 2816)),
  27. vision_patch_size=int(getattr(hf, "vision_patch_size", 14)),
  28. vision_rope_theta=float(getattr(hf, "vision_rope_theta", 10000.0)),
  29. vision_downsample_ratio=int(getattr(hf, "vision_downsample_ratio", 3)),
  30. vision_max_n_token=int(getattr(hf, "vision_max_n_token", 384)),
  31. vision_min_pixels=int(getattr(hf, "vision_min_pixels", 147456)),
  32. vision_max_wh_ratio=int(getattr(hf, "vision_max_wh_ratio", 8)),
  33. dim=int(getattr(hf, "hidden_size", 4096)),
  34. num_hash_layers=int(getattr(hf, "num_hash_layers", 0) or 0),
  35. )
  36. @MULTIMODAL_REGISTRY.register_processor(
  37. DSV4VisionMultiModalProcessor,
  38. info=DSV4VisionProcessingInfo,
  39. dummy_inputs=DSV4VisionDummyInputsBuilder,
  40. )
  41. class DeepseekV4VisionForCausalLM(DeepseekV4ForCausalLM):
  42. # ModelConfig.is_multimodal_model keys off this class attribute
  43. # (interfaces.supports_multimodal) — without it the OpenAI layer
  44. # answers "not a multimodal model" for image_url requests.
  45. supports_multimodal = True
  46. @classmethod
  47. def get_placeholder_str(cls, modality: str, i: int) -> str | None:
  48. # chat_utils asks the resolved model class for the per-image prompt
  49. # placeholder; the tokenizer encodes it to <|deepseek_image|> (id 129264).
  50. if modality.startswith("image"):
  51. from .encoding_dsv4 import IMAGE_PLACEHOLDER
  52. return IMAGE_PLACEHOLDER
  53. return None
  54. def __init__(self, *, vllm_config, prefix: str = ""):
  55. super().__init__(vllm_config=vllm_config, prefix=prefix)
  56. hf = vllm_config.model_config.hf_config
  57. args = _vision_args(hf)
  58. self._vision_args = args
  59. if args.vision_n_layers <= 0:
  60. self.vision = None
  61. self.aligner = None
  62. return
  63. self.vision = ViT(args)
  64. self.aligner = Aligner(args)
  65. self.image_start = nn.Parameter(torch.empty(args.dim))
  66. self.image_end = nn.Parameter(torch.empty(args.dim))
  67. self.image_newline = nn.Parameter(torch.empty(args.dim))
  68. self.image_pad = nn.Parameter(torch.empty(args.dim))
  69. def encode_image(self, patches: torch.Tensor, n_vit_h: int, n_vit_w: int) -> torch.Tensor:
  70. return self.aligner(self.vision(patches, n_vit_h, n_vit_w), n_vit_h, n_vit_w)
  71. def _merge_mm_embeddings(
  72. self,
  73. inputs_embeds: torch.Tensor,
  74. multimodal_embeddings,
  75. is_multimodal: torch.Tensor,
  76. ) -> torch.Tensor:
  77. # Mirrors interfaces._merge_multimodal_embeddings: fill masked rows in
  78. # order with the concatenated per-item embedding runs.
  79. flat = torch.cat([emb.reshape(-1) for emb in multimodal_embeddings])
  80. inputs_embeds.masked_scatter_(
  81. is_multimodal.to(inputs_embeds.device).unsqueeze(-1), flat
  82. )
  83. return inputs_embeds
  84. def embed_input_ids(
  85. self,
  86. input_ids: torch.Tensor,
  87. multimodal_embeddings=None,
  88. *,
  89. is_multimodal: torch.Tensor | None = None,
  90. ) -> torch.Tensor:
  91. if is_multimodal is not None:
  92. # Sentinel block ids may exceed vocab; zero them before lookup.
  93. input_ids = input_ids.masked_fill(
  94. is_multimodal.to(input_ids.device).unsqueeze(-1), 0
  95. )
  96. inputs_embeds = super().embed_input_ids(input_ids)
  97. if not multimodal_embeddings:
  98. return inputs_embeds
  99. return self._merge_mm_embeddings(
  100. inputs_embeds, multimodal_embeddings, is_multimodal
  101. )
  102. def embed_multimodal(self, **kwargs) -> tuple:
  103. patches = kwargs.get("image_patches")
  104. if patches is None or self.vision is None:
  105. return ()
  106. grids = kwargs["image_grids"].data
  107. types_all = kwargs["image_types"].data
  108. perm_all = kwargs["image_perm"].data
  109. t_counts = kwargs["image_type_counts"].data.tolist() if hasattr(kwargs["image_type_counts"], "tolist") else kwargs["image_type_counts"].data.tolist()
  110. perm_counts = kwargs["image_perm_counts"].data.tolist()
  111. n_items = int(grids.shape[0])
  112. out: list[torch.Tensor] = []
  113. p_off = t_off = q_off = 0
  114. start = self.image_start
  115. pad = self.image_pad
  116. newline = self.image_newline
  117. end = self.image_end
  118. for i in range(n_items):
  119. n_vit_h, n_vit_w, n_llm_h, n_llm_w = grids[i].tolist()
  120. p_count = n_vit_h * n_vit_w
  121. t_count = int(t_counts[i])
  122. q_count = int(perm_counts[i])
  123. item_types = types_all[t_off : t_off + t_count].tolist()
  124. item_perm = perm_all[q_off : q_off + q_count].to(torch.long)
  125. if q_count != sum(t == 2 for t in item_types):
  126. raise ValueError(
  127. "DSV4 vision perm/sentinel layout mismatch "
  128. f"({q_count} != {sum(t == 2 for t in item_types)})"
  129. )
  130. t_off += t_count
  131. q_off += q_count
  132. vit = self.encode_image(patches[p_off : p_off + p_count], n_vit_h, n_vit_w)
  133. p_off += p_count
  134. rows: list[torch.Tensor] = []
  135. vi = 0
  136. for t in item_types:
  137. if t == 0: # IMAGE_START
  138. rows.append(start)
  139. elif t == 1: # IMAGE_PAD
  140. rows.append(pad)
  141. elif t == 2: # IMAGE
  142. rows.append(vit[int(item_perm[vi])])
  143. vi += 1
  144. elif t == 3: # IMAGE_NEW_LINE
  145. rows.append(newline)
  146. else: # IMAGE_END
  147. rows.append(end)
  148. out.append(torch.stack(rows))
  149. return tuple(out)
  150. def forward(
  151. self,
  152. input_ids,
  153. positions,
  154. intermediate_tensors=None,
  155. inputs_embeds=None,
  156. **kwargs,
  157. ):
  158. # The runner embeds via embed_input_ids (mm merge) and hands us
  159. # merged inputs_embeds; lm_head and get_mtp_target_hidden_states
  160. # stay on the parent class.
  161. return super().forward(
  162. input_ids,
  163. positions,
  164. intermediate_tensors,
  165. inputs_embeds,
  166. )
  167. def load_weights(self, weights):
  168. if self.vision is None:
  169. return super().load_weights(weights)
  170. leftover = []
  171. stacked = {
  172. "vision.": self.vision,
  173. "aligner.": self.aligner,
  174. }
  175. params = {
  176. "image_start": self.image_start,
  177. "image_end": self.image_end,
  178. "image_newline": self.image_newline,
  179. "image_pad": self.image_pad,
  180. }
  181. for name, tensor in weights:
  182. if name in params:
  183. params[name].data.copy_(tensor)
  184. continue
  185. routed = False
  186. for prefix, module in stacked.items():
  187. if name.startswith(prefix):
  188. AutoWeightsLoader(module).load_weights(
  189. [(name[len(prefix) :], tensor)]
  190. )
  191. routed = True
  192. break
  193. if not routed:
  194. leftover.append((name, tensor))
  195. leftover = [
  196. pair
  197. for pair in leftover
  198. if not _skip_unmapped_gate(pair[0], self._vision_args)
  199. ]
  200. return super().load_weights(leftover)
  201. def _skip_unmapped_gate(name: str, args: SimpleNamespace) -> bool:
  202. # Hash MoE layers register tid2eid and leave e_score_correction_bias as
  203. # None. The B12X mapper still rewrites ffn.gate.bias onto that missing
  204. # name. bias_vl has no mapper at all.
  205. if name.endswith("ffn.gate.bias_vl"):
  206. return True
  207. if name.endswith("ffn.gate.bias"):
  208. parts = name.split(".")
  209. if (
  210. len(parts) >= 5
  211. and parts[0] == "layers"
  212. and parts[1].isdigit()
  213. and int(parts[1]) < args.num_hash_layers
  214. ):
  215. return True
  216. return False