[Bugfix][LoRA] Use an explicit capability flag for tower connector LoRA (#53092)

Signed-off-by: linitra24 <Joy25810@foxmail.com>

linitra24 committed Aug 21, 2026 at 17:45 UTC e85d1b69cf2f1c6101cfc7c799bb0c457cacc4b3
21 files changed +36 -2
vllm/lora/model_manager.py
+2 -2
@@ -201,8 +201,8 @@ class LoRAModelManager:
201 self.punica_wrapper_mapping[lm_prefix] = llm_punica_wrapper
202
203 # First, determine if the model supports tower connector LoRA.
204 - self.supports_tower_connector_lora = self.supports_mm and hasattr(
205 - self.model, "get_num_mm_encoder_tokens"
204 + self.supports_tower_connector_lora = (
205 + self.supports_mm and self.model.supports_tower_connector_lora
206 )
207
208 # Then, handle the case where the feature is disabled in the config.
vllm/model_executor/models/blip2.py
+2
@@ -525,6 +525,8 @@ class Blip2MultiModalProcessor(BaseMultiModalProcessor[Blip2ProcessingInfo]):
525 class Blip2ForConditionalGeneration(
526 nn.Module, SupportsLoRA, SupportsMultiModal, SupportsPP, SupportsQuant
527 ):
528 + supports_tower_connector_lora = True
529 +
530 @classmethod
531 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
532 if modality.startswith("image"):
vllm/model_executor/models/dots_ocr.py
+1
@@ -618,6 +618,7 @@ class DotsOCRForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA
618 "fc13": ["fc1", "fc3"],
619 }
620 supports_encoder_tp_data = True
621 + supports_tower_connector_lora = True
622
623 @classmethod
624 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
vllm/model_executor/models/gemma3_mm.py
+2
@@ -524,6 +524,8 @@ class Gemma3ForConditionalGeneration(
524 }
525 )
526
527 + supports_tower_connector_lora = True
528 +
529 @classmethod
530 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
531 if modality.startswith("image"):
vllm/model_executor/models/gemma4_mm.py
+1
@@ -978,6 +978,7 @@ class Gemma4ForConditionalGeneration(
978 # window instead of dropping them (which would make image attention
979 # causal-only for images larger than the sliding window).
980 mm_prefix_clamp_sliding_window: bool = True
981 + supports_tower_connector_lora = True
982
983 packed_modules_mapping = {
984 "qkv_proj": [
vllm/model_executor/models/glm4_1v.py
+1
@@ -1755,6 +1755,7 @@ class Glm4vForConditionalGeneration(
1755 )
1756
1757 supports_encoder_tp_data = True
1758 + supports_tower_connector_lora = True
1759
1760 @classmethod
1761 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
vllm/model_executor/models/idefics3.py
+2
@@ -503,6 +503,8 @@ class Idefics3ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsLo
503 ],
504 }
505
506 + supports_tower_connector_lora = True
507 +
508 @classmethod
509 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
510 if modality.startswith("image"):
vllm/model_executor/models/interfaces.py
+6
@@ -167,6 +167,12 @@ class SupportsMultiModal(SupportsMultiModalEmbeddings, Protocol):
167 `multimodal_config.mm_device_do_normalize`.
168 """
169
170 + supports_tower_connector_lora: ClassVar[bool] = False
171 + """
172 + A flag that indicates whether this model supports
173 + `lora_config.enable_tower_connector_lora`.
174 + """
175 +
176 requires_raw_input_tokens: ClassVar[bool] = False
177 """
178 A flag that indicates this model processes input id tokens
vllm/model_executor/models/internvl.py
+1
@@ -553,6 +553,7 @@ class InternVLChatModel(
553 SupportsEncoderCudaGraph,
554 ):
555 supports_encoder_tp_data = True
556 + supports_tower_connector_lora = True
557
558 hf_to_vllm_mapper = WeightsMapper(
559 orig_to_new_prefix=dict.fromkeys(
vllm/model_executor/models/keye.py
+2
@@ -1235,6 +1235,8 @@ class BaseKeyeModule(nn.Module, SupportsMultiModal):
1235 }
1236 )
1237
1238 + supports_tower_connector_lora = True
1239 +
1240 @classmethod
1241 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
1242 if modality.startswith("image"):
vllm/model_executor/models/llava.py
+2
@@ -520,6 +520,8 @@ class LlavaForConditionalGeneration(
520 }
521 )
522
523 + supports_tower_connector_lora = True
524 +
525 @classmethod
526 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
527 if modality.startswith("image"):
vllm/model_executor/models/llava_next.py
+1
@@ -231,6 +231,7 @@ class LlavaNextMultiModalProcessor(BaseLlavaNextMultiModalProcessor[_I]):
231 class LlavaNextForConditionalGeneration(
232 nn.Module, SupportsLoRA, SupportsMultiModal, SupportsPP
233 ):
234 + supports_tower_connector_lora = True
235 packed_modules_mapping = {
236 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
237 "gate_up_proj": ["gate_proj", "up_proj"],
vllm/model_executor/models/llava_next_video.py
+2
@@ -323,6 +323,8 @@ class LlavaNextVideoForConditionalGeneration(
323 }
324 )
325
326 + supports_tower_connector_lora = True
327 +
328 @classmethod
329 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
330 if modality.startswith("video"):
vllm/model_executor/models/mllama4.py
+1
@@ -740,6 +740,7 @@ class Llama4ForConditionalGeneration(
740 }
741
742 supports_encoder_tp_data = True
743 + supports_tower_connector_lora = True
744
745 @classmethod
746 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
vllm/model_executor/models/paligemma.py
+2
@@ -274,6 +274,8 @@ class PaliGemmaForConditionalGeneration(
274 }
275 )
276
277 + supports_tower_connector_lora = True
278 +
279 @classmethod
280 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
281 if modality.startswith("image"):
vllm/model_executor/models/pixtral.py
+2
@@ -309,6 +309,8 @@ class PixtralForConditionalGeneration(
309 "gate_up_proj": ["gate_proj", "up_proj"],
310 }
311
312 + supports_tower_connector_lora = True
313 +
314 @classmethod
315 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
316 if modality.startswith("image"):
vllm/model_executor/models/qwen2_5_vl.py
+1
@@ -1256,6 +1256,7 @@ class Qwen2_5_VLForConditionalGeneration(
1256
1257 supports_encoder_tp_data = True
1258 supports_mm_device_do_normalize = True
1259 + supports_tower_connector_lora = True
1260
1261 def iter_mm_grid_thw(
1262 self, mm_features: list[MultiModalFeatureSpec]
vllm/model_executor/models/qwen2_vl.py
+1
@@ -1194,6 +1194,7 @@ class Qwen2VLForConditionalGeneration(
1194
1195 supports_encoder_tp_data = True
1196 supports_mm_device_do_normalize = True
1197 + supports_tower_connector_lora = True
1198
1199 def iter_mm_grid_thw(
1200 self, mm_features: list[MultiModalFeatureSpec]
vllm/model_executor/models/qwen3_asr.py
+1
@@ -361,6 +361,7 @@ class Qwen3ASRForConditionalGeneration(
361 }
362
363 supported_languages = ISO639_1_SUPPORTED_LANGS
364 + supports_tower_connector_lora = True
365
366 hf_to_vllm_mapper = WeightsMapper(
367 orig_to_new_prefix={
vllm/model_executor/models/qwen3_vl.py
+1
@@ -1753,6 +1753,7 @@ class Qwen3VLForConditionalGeneration(
1753 }
1754
1755 supports_encoder_tp_data = True
1756 + supports_tower_connector_lora = True
1757
1758 supported_video_pruning_methods = ("evs", "vidcom2")
1759
vllm/model_executor/models/ultravox.py
+2
@@ -610,6 +610,8 @@ class UltravoxModel(nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA):
610 }
611 )
612
613 + supports_tower_connector_lora = True
614 +
615 @classmethod
616 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
617 if modality.startswith("audio"):