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