[LoRA] Avoid false target matches for unsupported module types (#52313)

Signed-off-by: linitra24 <Joy25810@foxmail.com> Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>

linitra24 committed Aug 20, 2026 at 00:48 UTC 9b5f3454f2be4c2c0f17c84a78af25a1d550d446
3 files changed +39 -51
tests/lora/test_lora_utils.py
+1 -31
@@ -1,37 +1,7 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 -
5 -from vllm.lora.utils import is_in_target_modules, is_supported_lora_module
6 -
7 -
8 -class TestIsSupportedLoraModule:
9 - """Tests for is_supported_lora_module (model-definition check)."""
10 -
11 - def test_suffix_match(self):
12 - assert is_supported_lora_module(
13 - "model.layers.0.self_attn.o_proj", ["o_proj", "q_proj"]
14 - )
15 -
16 - def test_no_match(self):
17 - assert not is_supported_lora_module(
18 - "model.layers.0.self_attn.o_proj", ["q_proj", "k_proj"]
19 - )
20 -
21 - def test_exact_match(self):
22 - assert is_supported_lora_module("o_proj", ["o_proj"])
23 -
24 - def test_regex_suffix_matching(self):
25 - """Regex anchors to end — partial suffix should not match."""
26 - assert not is_supported_lora_module("model.layers.0.self_attn.o_proj", ["proj"])
27 -
28 - def test_empty_supported_modules(self):
29 - assert not is_supported_lora_module("model.layers.0.self_attn.o_proj", [])
30 -
31 - def test_multiple_supported_modules(self):
32 - supported = ["q_proj", "k_proj", "v_proj", "o_proj"]
33 - assert is_supported_lora_module("model.layers.0.self_attn.v_proj", supported)
34 - assert not is_supported_lora_module("model.layers.0.mlp.gate_proj", supported)
4 +from vllm.lora.utils import is_in_target_modules
5
6
7 class TestIsInTargetModules:
vllm/lora/model_manager.py
+18 -4
@@ -418,7 +418,16 @@ class LoRAModelManager:
418 if isinstance(module, PPMissingLayer):
419 continue
420
421 - if not self._match_target_modules(module_name):
421 + target_modules = self.lora_config.target_modules
422 + is_configured_target = target_modules is not None and is_in_target_modules(
423 + module_name,
424 + target_modules,
425 + self.packed_modules_mapping,
426 + )
427 + if (
428 + not self._match_target_modules(module_name, module)
429 + and not is_configured_target
430 + ):
431 continue
432
433 punica_wrapper = self._get_punica_wrapper(module_name)
@@ -565,7 +574,7 @@ class LoRAModelManager:
574 model = LoRAModel(lora_id, rank, {})
575 for module_name, module in self.model.named_modules():
576 if (
568 - not self._match_target_modules(module_name)
577 + not self._match_target_modules(module_name, module)
578 or not isinstance(module, BaseLayerWithLoRA)
579 or self._get_punica_wrapper(module_name) is None
580 ):
@@ -701,7 +710,7 @@ class LoRAModelManager:
710 )
711 return adjusted_rank
712
704 - def _match_target_modules(self, module_name: str) -> bool:
713 + def _match_target_modules(self, module_name: str, module: nn.Module) -> bool:
714 """Check if a module should have LoRA applied.
715
716 This method first checks if the module is in vLLM's supported LoRA
@@ -711,11 +720,16 @@ class LoRAModelManager:
720 Args:
721 module_name: Full dot-separated module name (e.g.,
722 "model.layers.0.self_attn.o_proj")
723 + module: Runtime module associated with ``module_name``.
724
725 Returns:
726 True if LoRA should be applied to this module, False otherwise.
727 """
718 - if not is_supported_lora_module(module_name, self.supported_lora_modules):
728 + if not is_supported_lora_module(
729 + module_name,
730 + module,
731 + self.supported_lora_modules,
732 + ):
733 return False
734 return is_in_target_modules(
735 module_name,
vllm/lora/utils.py
+20 -16
@@ -4,7 +4,6 @@
4 import os
5 from typing import TYPE_CHECKING
6
7 -import regex as re
7 from huggingface_hub.utils import HfHubHTTPError, HFValidationError
8 from torch import nn
9 from transformers import PretrainedConfig
@@ -33,8 +32,10 @@ from vllm.lora.layers import (
32 RowParallelLinearWithShardedLoRA,
33 VocabParallelEmbeddingWithLoRA,
34 )
35 +from vllm.model_executor.custom_op import maybe_get_oot_by_class
36 from vllm.model_executor.layers.fused_moe import MoERunner
37 from vllm.model_executor.layers.linear import LinearBase
38 +from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
39 from vllm.model_executor.utils import get_moe_expert_mapping, get_packed_modules_mapping
40 from vllm.transformers_utils.repo_utils import hf_api
41
@@ -230,11 +231,7 @@ def get_supported_lora_modules(model: nn.Module) -> list[str]:
231 for name in embedding_modules:
232 supported_lora_modules.add(name)
233
233 - # get all the linear subfixes.
234 - if isinstance(module, (LinearBase,)):
235 - supported_lora_modules.add(name.split(".")[-1])
236 -
237 - if isinstance(module, (MoERunner,)):
234 + if isinstance(module, (LinearBase, MoERunner)):
235 supported_lora_modules.add(name.split(".")[-1])
236
237 return list(supported_lora_modules)
@@ -242,29 +239,36 @@ def get_supported_lora_modules(model: nn.Module) -> list[str]:
239
240 def is_supported_lora_module(
241 module_name: str,
242 + module: nn.Module,
243 supported_lora_modules: list[str],
244 ) -> bool:
245 """Check if a module is in the model's supported LoRA modules.
246
249 - Uses regex suffix matching against the model-defined supported modules
250 - list (e.g., matching "model.layers.0.self_attn.o_proj" against
251 - "o_proj").
247 + The module name must match a model-supported suffix, and the runtime
248 + module must belong to a module family handled by LoRA.
249
250 Args:
251 module_name: Full dot-separated module name.
252 + module: Runtime module associated with ``module_name``.
253 supported_lora_modules: List of module suffixes supported by the
254 model.
255
256 Returns:
257 True if the module is supported, False otherwise.
258 """
261 - return any(
262 - re.match(
263 - r".*\.{target_module}$".format(target_module=target_module),
264 - module_name,
265 - )
266 - or target_module == module_name
267 - for target_module in supported_lora_modules
259 + module_suffix = module_name.rsplit(".", 1)[-1]
260 + if module_suffix not in supported_lora_modules:
261 + return False
262 +
263 + return isinstance(
264 + module,
265 + (
266 + LinearBase,
267 + MoERunner,
268 + VocabParallelEmbedding,
269 + maybe_get_oot_by_class(VocabParallelEmbedding),
270 + BaseLayerWithLoRA,
271 + ),
272 )
273
274