|
@@ -2,19 +2,23 @@
|
|
|
|
|
|
|
|
Loads official `vision.*` / `aligner.*` / image sentinel tensors and merges
|
|
Loads official `vision.*` / `aligner.*` / image sentinel tensors and merges
|
|
|
ViT embeddings on prefill. Language decode stays on the B12X DeepSeek V4 path.
|
|
ViT embeddings on prefill. Language decode stays on the B12X DeepSeek V4 path.
|
|
|
|
|
+
|
|
|
|
|
+The OpenAI multimodal branch (processor trio + embed-merge hooks) is gated
|
|
|
|
|
+behind VLLM_DSV4_MM=1: the fork's V2 "always-embed" mm arm routes through
|
|
|
|
|
+this override with a [B, T]-ranks ids slice and currently degenerates
|
|
|
|
|
+logits, so with VLLM_DSV4_MM unset (the default) the lane boots text-only —
|
|
|
|
|
+the verified state — and VLLM_DSV4_MM=1 opts into the experimental path.
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
+import os
|
|
|
from types import SimpleNamespace
|
|
from types import SimpleNamespace
|
|
|
from typing import Any
|
|
from typing import Any
|
|
|
|
|
|
|
|
import torch
|
|
import torch
|
|
|
import torch.nn as nn
|
|
import torch.nn as nn
|
|
|
|
|
|
|
|
-import torch
|
|
|
|
|
-import torch.nn as nn
|
|
|
|
|
-
|
|
|
|
|
from vllm.model_executor.models.utils import AutoWeightsLoader
|
|
from vllm.model_executor.models.utils import AutoWeightsLoader
|
|
|
from vllm.models.deepseek_v4 import DeepseekV4ForCausalLM
|
|
from vllm.models.deepseek_v4 import DeepseekV4ForCausalLM
|
|
|
from vllm.multimodal import MULTIMODAL_REGISTRY
|
|
from vllm.multimodal import MULTIMODAL_REGISTRY
|
|
@@ -26,6 +30,19 @@ from .processor import (
|
|
|
)
|
|
)
|
|
|
from .vision import Aligner, ViT
|
|
from .vision import Aligner, ViT
|
|
|
|
|
|
|
|
|
|
+_MM_ON = os.environ.get("VLLM_DSV4_MM", "0") == "1"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _maybe_mm(cls):
|
|
|
|
|
+ """Register the OpenAI multimodal processor trio only when mm is on."""
|
|
|
|
|
+ if not _MM_ON:
|
|
|
|
|
+ return cls
|
|
|
|
|
+ return MULTIMODAL_REGISTRY.register_processor(
|
|
|
|
|
+ DSV4VisionMultiModalProcessor,
|
|
|
|
|
+ info=DSV4VisionProcessingInfo,
|
|
|
|
|
+ dummy_inputs=DSV4VisionDummyInputsBuilder,
|
|
|
|
|
+ )(cls)
|
|
|
|
|
+
|
|
|
|
|
|
|
|
def _vision_args(hf: Any) -> SimpleNamespace:
|
|
def _vision_args(hf: Any) -> SimpleNamespace:
|
|
|
return SimpleNamespace(
|
|
return SimpleNamespace(
|
|
@@ -44,22 +61,25 @@ def _vision_args(hf: Any) -> SimpleNamespace:
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
-@MULTIMODAL_REGISTRY.register_processor(
|
|
|
|
|
- DSV4VisionMultiModalProcessor,
|
|
|
|
|
- info=DSV4VisionProcessingInfo,
|
|
|
|
|
- dummy_inputs=DSV4VisionDummyInputsBuilder,
|
|
|
|
|
-)
|
|
|
|
|
|
|
+@_maybe_mm
|
|
|
class DeepseekV4VisionForCausalLM(DeepseekV4ForCausalLM):
|
|
class DeepseekV4VisionForCausalLM(DeepseekV4ForCausalLM):
|
|
|
# ModelConfig.is_multimodal_model keys off this class attribute
|
|
# ModelConfig.is_multimodal_model keys off this class attribute
|
|
|
# (interfaces.supports_multimodal) — without it the OpenAI layer
|
|
# (interfaces.supports_multimodal) — without it the OpenAI layer
|
|
|
- # answers "not a multimodal model" for image_url requests.
|
|
|
|
|
- supports_multimodal = True
|
|
|
|
|
|
|
+ # answers "not a multimodal model" for image_url requests. Gated by
|
|
|
|
|
+ # _maybe_mm so the text-only default lane stays verified.
|
|
|
|
|
+ supports_multimodal = _MM_ON
|
|
|
|
|
+ # The fork's hash MoE routes by token id (tid2eid gather), so the merged
|
|
|
|
|
+ # mm path must keep input_ids alongside the merged embeddings.
|
|
|
|
|
+ requires_raw_input_tokens = _MM_ON
|
|
|
|
|
+ supports_multimodal_raw_input_only = False
|
|
|
|
|
+ supports_encoder_tp_data = False
|
|
|
|
|
+ supports_mm_device_do_normalize = False
|
|
|
|
|
|
|
|
@classmethod
|
|
@classmethod
|
|
|
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
|
|
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
|
|
|
# chat_utils asks the resolved model class for the per-image prompt
|
|
# chat_utils asks the resolved model class for the per-image prompt
|
|
|
# placeholder; the tokenizer encodes it to <|deepseek_image|> (id 129264).
|
|
# placeholder; the tokenizer encodes it to <|deepseek_image|> (id 129264).
|
|
|
- if modality.startswith("image"):
|
|
|
|
|
|
|
+ if _MM_ON and modality.startswith("image"):
|
|
|
from .encoding_dsv4 import IMAGE_PLACEHOLDER
|
|
from .encoding_dsv4 import IMAGE_PLACEHOLDER
|
|
|
|
|
|
|
|
return IMAGE_PLACEHOLDER
|
|
return IMAGE_PLACEHOLDER
|
|
@@ -84,20 +104,6 @@ class DeepseekV4VisionForCausalLM(DeepseekV4ForCausalLM):
|
|
|
def encode_image(self, patches: torch.Tensor, n_vit_h: int, n_vit_w: int) -> torch.Tensor:
|
|
def encode_image(self, patches: torch.Tensor, n_vit_h: int, n_vit_w: int) -> torch.Tensor:
|
|
|
return self.aligner(self.vision(patches, n_vit_h, n_vit_w), n_vit_h, n_vit_w)
|
|
return self.aligner(self.vision(patches, n_vit_h, n_vit_w), n_vit_h, n_vit_w)
|
|
|
|
|
|
|
|
- def _merge_mm_embeddings(
|
|
|
|
|
- self,
|
|
|
|
|
- inputs_embeds: torch.Tensor,
|
|
|
|
|
- multimodal_embeddings,
|
|
|
|
|
- is_multimodal: torch.Tensor,
|
|
|
|
|
- ) -> torch.Tensor:
|
|
|
|
|
- # Mirrors interfaces._merge_multimodal_embeddings: fill masked rows in
|
|
|
|
|
- # order with the concatenated per-item embedding runs.
|
|
|
|
|
- flat = torch.cat([emb.reshape(-1) for emb in multimodal_embeddings])
|
|
|
|
|
- inputs_embeds.masked_scatter_(
|
|
|
|
|
- is_multimodal.to(inputs_embeds.device).unsqueeze(-1), flat
|
|
|
|
|
- )
|
|
|
|
|
- return inputs_embeds
|
|
|
|
|
-
|
|
|
|
|
def embed_input_ids(
|
|
def embed_input_ids(
|
|
|
self,
|
|
self,
|
|
|
input_ids: torch.Tensor,
|
|
input_ids: torch.Tensor,
|
|
@@ -105,17 +111,25 @@ class DeepseekV4VisionForCausalLM(DeepseekV4ForCausalLM):
|
|
|
*,
|
|
*,
|
|
|
is_multimodal: torch.Tensor | None = None,
|
|
is_multimodal: torch.Tensor | None = None,
|
|
|
) -> torch.Tensor:
|
|
) -> torch.Tensor:
|
|
|
|
|
+ # The fork's embed output follows the input rank: [B, T, H] from the
|
|
|
|
|
+ # speculator's [B, T] ids, [T, H] from plain [T] ids. mm rows must
|
|
|
|
|
+ # land on the same rows either way (they never exceed inner dims).
|
|
|
if is_multimodal is not None:
|
|
if is_multimodal is not None:
|
|
|
# Sentinel block ids may exceed vocab; zero them before lookup.
|
|
# Sentinel block ids may exceed vocab; zero them before lookup.
|
|
|
input_ids = input_ids.masked_fill(
|
|
input_ids = input_ids.masked_fill(
|
|
|
is_multimodal.to(input_ids.device).unsqueeze(-1), 0
|
|
is_multimodal.to(input_ids.device).unsqueeze(-1), 0
|
|
|
)
|
|
)
|
|
|
- inputs_embeds = super().embed_input_ids(input_ids)
|
|
|
|
|
- if not multimodal_embeddings:
|
|
|
|
|
- return inputs_embeds
|
|
|
|
|
- return self._merge_mm_embeddings(
|
|
|
|
|
- inputs_embeds, multimodal_embeddings, is_multimodal
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ out = super().embed_input_ids(input_ids)
|
|
|
|
|
+ if multimodal_embeddings:
|
|
|
|
|
+ mm_stack = torch.cat(multimodal_embeddings) # [N, H]
|
|
|
|
|
+ if out.dim() == 3:
|
|
|
|
|
+ out[is_multimodal] = mm_stack.to(out.dtype)
|
|
|
|
|
+ else:
|
|
|
|
|
+ out.masked_scatter_(
|
|
|
|
|
+ is_multimodal.to(out.device).unsqueeze(-1),
|
|
|
|
|
+ mm_stack.reshape(-1),
|
|
|
|
|
+ )
|
|
|
|
|
+ return out
|
|
|
|
|
|
|
|
def embed_multimodal(self, **kwargs) -> tuple:
|
|
def embed_multimodal(self, **kwargs) -> tuple:
|
|
|
patches = kwargs.get("image_patches")
|
|
patches = kwargs.get("image_patches")
|
|
@@ -124,7 +138,7 @@ class DeepseekV4VisionForCausalLM(DeepseekV4ForCausalLM):
|
|
|
grids = kwargs["image_grids"].data
|
|
grids = kwargs["image_grids"].data
|
|
|
types_all = kwargs["image_types"].data
|
|
types_all = kwargs["image_types"].data
|
|
|
perm_all = kwargs["image_perm"].data
|
|
perm_all = kwargs["image_perm"].data
|
|
|
- t_counts = kwargs["image_type_counts"].data.tolist() if hasattr(kwargs["image_type_counts"], "tolist") else kwargs["image_type_counts"].data.tolist()
|
|
|
|
|
|
|
+ t_counts = kwargs["image_type_counts"].data.tolist()
|
|
|
perm_counts = kwargs["image_perm_counts"].data.tolist()
|
|
perm_counts = kwargs["image_perm_counts"].data.tolist()
|
|
|
n_items = int(grids.shape[0])
|
|
n_items = int(grids.shape[0])
|
|
|
|
|
|
|
@@ -179,6 +193,11 @@ class DeepseekV4VisionForCausalLM(DeepseekV4ForCausalLM):
|
|
|
# The runner embeds via embed_input_ids (mm merge) and hands us
|
|
# The runner embeds via embed_input_ids (mm merge) and hands us
|
|
|
# merged inputs_embeds; lm_head and get_mtp_target_hidden_states
|
|
# merged inputs_embeds; lm_head and get_mtp_target_hidden_states
|
|
|
# stay on the parent class.
|
|
# stay on the parent class.
|
|
|
|
|
+ if not _MM_ON and inputs_embeds is not None:
|
|
|
|
|
+ # text-only belt-and-braces: without the mm gate the parent never
|
|
|
|
|
+ # sees runner-generated embeds, but if one sneaks in, sync the
|
|
|
|
|
+ # token path instead so hidden shapes stay native.
|
|
|
|
|
+ inputs_embeds = None
|
|
|
return super().forward(
|
|
return super().forward(
|
|
|
input_ids,
|
|
input_ids,
|
|
|
positions,
|
|
positions,
|