From dd4c8410707bdb833b155ff3258ce5271c0f476e Mon Sep 17 00:00:00 2001 From: linitra24 Date: Sun, 13 Sep 2026 22:10:54 +0800 Subject: [PATCH] [LoRA] Add LoRA support for DeepSeek-V4 Flash Vision (#55897) Signed-off-by: linitra24 --- docs/models/supported_models.md | 2 +- vllm/lora/ops/triton_ops/lora_shrink_op.py | 2 +- vllm/models/deepseek_v4/common/vl_model.py | 41 +++++++++++++++++++++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/docs/models/supported_models.md b/docs/models/supported_models.md index d06154fb5b..81f4fb7df1 100644 --- a/docs/models/supported_models.md +++ b/docs/models/supported_models.md @@ -547,7 +547,7 @@ These models primarily accept the [`LLM.generate`](./generative_models.md#llmgen | `DeepseekVLV2ForCausalLM` | DeepSeek-VL2 | T + I+ | `deepseek-ai/deepseek-vl2-tiny`, `deepseek-ai/deepseek-vl2-small`, `deepseek-ai/deepseek-vl2`, etc. | | ✅︎ | | `DeepseekOCRForCausalLM` | DeepSeek-OCR | T + I+ | `deepseek-ai/DeepSeek-OCR`, etc. | ✅︎ | ✅︎ | | `DeepseekOCR2ForCausalLM` | DeepSeek-OCR-2 | T + I+ | `deepseek-ai/DeepSeek-OCR-2`, etc. | ✅︎ | ✅︎ | -| `DeepseekV4ForConditionalGeneration` | DeepSeek-V4 (vision) | T + I+ | `deepseek-ai/DeepSeek-V4-Flash-Vision-Exp` | | ✅︎ | +| `DeepseekV4ForConditionalGeneration` | DeepSeek-V4 (vision) | T + I+ | `deepseek-ai/DeepSeek-V4-Flash-Vision-Exp` | ✅︎ | ✅︎ | | `Eagle2_5_VLForConditionalGeneration` | Eagle2.5-VL | T + IE+ | `nvidia/Eagle2.5-8B`, etc. | ✅︎ | ✅︎ | | `Ernie4_5_VLMoeForConditionalGeneration` | Ernie4.5-VL | T + I+/ V+ | `baidu/ERNIE-4.5-VL-28B-A3B-PT`, `baidu/ERNIE-4.5-VL-424B-A47B-PT` | | ✅︎ | | `Exaone4_5_ForConditionalGeneration` | EXAONE-4.5 | T + IE+ | `LGAI-EXAONE/EXAONE-4.5-33B`, etc. | ✅︎ | ✅︎ | diff --git a/vllm/lora/ops/triton_ops/lora_shrink_op.py b/vllm/lora/ops/triton_ops/lora_shrink_op.py index 4144c16ead..d05c8f7c2a 100644 --- a/vllm/lora/ops/triton_ops/lora_shrink_op.py +++ b/vllm/lora/ops/triton_ops/lora_shrink_op.py @@ -179,7 +179,7 @@ def _lora_shrink( assert weight.dtype in [torch.float16, torch.bfloat16] assert inputs.size(1) == lora_a_weights[0].size(-1) - assert inputs.is_contiguous() + inputs = inputs.contiguous() assert output_tensor.is_contiguous() # metadata sanity check diff --git a/vllm/models/deepseek_v4/common/vl_model.py b/vllm/models/deepseek_v4/common/vl_model.py index 4cccb85a83..97701cc2d5 100644 --- a/vllm/models/deepseek_v4/common/vl_model.py +++ b/vllm/models/deepseek_v4/common/vl_model.py @@ -25,9 +25,11 @@ from torch import nn from vllm.model_executor.models.interfaces import ( MultiModalEmbeddings, SupportsEagle3, + SupportsLoRA, SupportsMultiModal, SupportsPP, ) +from vllm.model_executor.models.module_mapping import MultiModelKeys from vllm.model_executor.models.utils import ( AutoWeightsLoader, WeightsMapper, @@ -35,6 +37,7 @@ from vllm.model_executor.models.utils import ( maybe_prefix, ) from vllm.multimodal import MULTIMODAL_REGISTRY +from vllm.multimodal.inputs import MultiModalKwargsItem from .mm_preprocess import ( IMAGE_PLACEHOLDER, @@ -81,7 +84,7 @@ def _make_deepseek_v4_vl_weights_mapper( dummy_inputs=DeepseekV4VLDummyInputsBuilder, ) class DeepseekV4ForConditionalGeneration( - nn.Module, SupportsMultiModal, SupportsPP, SupportsEagle3 + nn.Module, SupportsMultiModal, SupportsPP, SupportsEagle3, SupportsLoRA ): """Multimodal entry point for DeepSeek-V4 checkpoints with a vision tower. @@ -89,9 +92,19 @@ class DeepseekV4ForConditionalGeneration( delegates through ``language_model`` via the protocol defaults. """ + packed_modules_mapping = { + "gate_up_proj": ["w1", "w3"], + "fused_wqa_wkv": ["wq_a", "wkv"], + "fused_wkv_wgate": ["wkv", "wgate"], + # for visual encoder + "wqkv": ["wqkv"], + "w1": ["w1"], + } + # The MoE router needs raw token ids to detect image sentinel tokens # (borrowed reserved ids, see common/mm_preprocess.py) and apply bias_vl. requires_raw_input_tokens = True + supports_tower_connector_lora = True @classmethod def get_placeholder_str(cls, modality: str, i: int) -> str | None: @@ -329,3 +342,29 @@ class DeepseekV4ForConditionalGeneration( return self.language_model.process_weights_after_loading() self._weights_finalized = True + + def get_mm_mapping(self) -> MultiModelKeys: + """Get the module prefixes in the multimodal model.""" + return MultiModelKeys.from_string_field( + language_model="language_model", + connector="aligner", + tower_model="vision.", + ) + + def get_mm_lora_token_counts( + self, + *, + modality: str, + mm_kwargs: MultiModalKwargsItem | None, + num_mm_embeds: int, + ) -> tuple[int, int | None]: + if modality != "image": + raise ValueError(f"Unsupported modality: {modality!r}") + + patches = mm_kwargs.get("patches") if mm_kwargs else None + if patches is not None and isinstance(patches.data, torch.Tensor): + tower_tokens = patches.data.shape[0] + else: + tower_tokens = num_mm_embeds * self.config.vision_downsample_ratio**2 + + return tower_tokens, num_mm_embeds