model.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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. from vllm.model_executor.models.utils import AutoWeightsLoader
  11. from vllm.models.deepseek_v4 import DeepseekV4ForCausalLM
  12. from .vision import Aligner, ViT
  13. def _vision_args(hf: Any) -> SimpleNamespace:
  14. return SimpleNamespace(
  15. vision_n_layers=int(getattr(hf, "vision_n_layers", 0) or 0),
  16. vision_dim=int(getattr(hf, "vision_dim", 1024)),
  17. vision_n_heads=int(getattr(hf, "vision_n_heads", 16)),
  18. vision_inter_dim=int(getattr(hf, "vision_inter_dim", 2816)),
  19. vision_patch_size=int(getattr(hf, "vision_patch_size", 14)),
  20. vision_rope_theta=float(getattr(hf, "vision_rope_theta", 10000.0)),
  21. vision_downsample_ratio=int(getattr(hf, "vision_downsample_ratio", 3)),
  22. vision_max_n_token=int(getattr(hf, "vision_max_n_token", 384)),
  23. vision_min_pixels=int(getattr(hf, "vision_min_pixels", 147456)),
  24. vision_max_wh_ratio=int(getattr(hf, "vision_max_wh_ratio", 8)),
  25. dim=int(getattr(hf, "hidden_size", 4096)),
  26. num_hash_layers=int(getattr(hf, "num_hash_layers", 0) or 0),
  27. )
  28. class DeepseekV4VisionForCausalLM(DeepseekV4ForCausalLM):
  29. # ModelConfig.is_multimodal_model keys off this class attribute
  30. # (interfaces.supports_multimodal) — without it the OpenAI layer
  31. # answers "not a multimodal model" for image_url requests.
  32. supports_multimodal = True
  33. def __init__(self, *, vllm_config, prefix: str = ""):
  34. super().__init__(vllm_config=vllm_config, prefix=prefix)
  35. hf = vllm_config.model_config.hf_config
  36. args = _vision_args(hf)
  37. self._vision_args = args
  38. if args.vision_n_layers <= 0:
  39. self.vision = None
  40. self.aligner = None
  41. return
  42. self.vision = ViT(args)
  43. self.aligner = Aligner(args)
  44. self.image_start = nn.Parameter(torch.empty(args.dim))
  45. self.image_end = nn.Parameter(torch.empty(args.dim))
  46. self.image_newline = nn.Parameter(torch.empty(args.dim))
  47. self.image_pad = nn.Parameter(torch.empty(args.dim))
  48. def encode_image(self, patches: torch.Tensor, n_vit_h: int, n_vit_w: int) -> torch.Tensor:
  49. return self.aligner(self.vision(patches, n_vit_h, n_vit_w), n_vit_h, n_vit_w)
  50. def forward(
  51. self,
  52. input_ids,
  53. positions,
  54. intermediate_tensors=None,
  55. inputs_embeds=None,
  56. **kwargs,
  57. ):
  58. # Spec-decode extra kwargs must not TypeError. lm_head and
  59. # get_mtp_target_hidden_states stay on the parent class.
  60. return super().forward(
  61. input_ids,
  62. positions,
  63. intermediate_tensors,
  64. inputs_embeds,
  65. )
  66. def load_weights(self, weights):
  67. if self.vision is None:
  68. return super().load_weights(weights)
  69. leftover = []
  70. stacked = {
  71. "vision.": self.vision,
  72. "aligner.": self.aligner,
  73. }
  74. params = {
  75. "image_start": self.image_start,
  76. "image_end": self.image_end,
  77. "image_newline": self.image_newline,
  78. "image_pad": self.image_pad,
  79. }
  80. for name, tensor in weights:
  81. if name in params:
  82. params[name].data.copy_(tensor)
  83. continue
  84. routed = False
  85. for prefix, module in stacked.items():
  86. if name.startswith(prefix):
  87. AutoWeightsLoader(module).load_weights(
  88. [(name[len(prefix) :], tensor)]
  89. )
  90. routed = True
  91. break
  92. if not routed:
  93. leftover.append((name, tensor))
  94. leftover = [
  95. pair
  96. for pair in leftover
  97. if not _skip_unmapped_gate(pair[0], self._vision_args)
  98. ]
  99. return super().load_weights(leftover)
  100. def _skip_unmapped_gate(name: str, args: SimpleNamespace) -> bool:
  101. # Hash MoE layers register tid2eid and leave e_score_correction_bias as
  102. # None. The B12X mapper still rewrites ffn.gate.bias onto that missing
  103. # name. bias_vl has no mapper at all.
  104. if name.endswith("ffn.gate.bias_vl"):
  105. return True
  106. if name.endswith("ffn.gate.bias"):
  107. parts = name.split(".")
  108. if (
  109. len(parts) >= 5
  110. and parts[0] == "layers"
  111. and parts[1].isdigit()
  112. and int(parts[1]) < args.num_hash_layers
  113. ):
  114. return True
  115. return False