utils.py 10.5 KB
Newer Older
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3

4
import os
5
from typing import TYPE_CHECKING, Optional
6

7
import huggingface_hub
8
import regex as re
9
10
11
12
13
14
from huggingface_hub.utils import (
    EntryNotFoundError,
    HfHubHTTPError,
    HFValidationError,
    RepositoryNotFoundError,
)
15
from torch import nn
16
from transformers import PretrainedConfig
17

18
from vllm.config.lora import LoRAConfig
19
from vllm.logger import init_logger
20

21
# being imported for _all_lora_classes below
22
23
24
25
from vllm.lora.layers import (
    BaseLayerWithLoRA,
    ColumnParallelLinearWithLoRA,
    ColumnParallelLinearWithShardedLoRA,
26
    FusedMoE3DWithLoRA,
27
    FusedMoEWithLoRA,
28
29
30
31
32
33
34
35
36
37
38
39
    LogitsProcessorWithLoRA,
    MergedColumnParallelLinearWithLoRA,
    MergedColumnParallelLinearWithShardedLoRA,
    MergedQKVParallelLinearWithLoRA,
    MergedQKVParallelLinearWithShardedLoRA,
    QKVParallelLinearWithLoRA,
    QKVParallelLinearWithShardedLoRA,
    ReplicatedLinearWithLoRA,
    RowParallelLinearWithLoRA,
    RowParallelLinearWithShardedLoRA,
    VocabParallelEmbeddingWithLoRA,
)
40
from vllm.model_executor.layers.fused_moe import FusedMoE
41
from vllm.model_executor.layers.linear import LinearBase
42
from vllm.model_executor.utils import get_moe_expert_mapping, get_packed_modules_mapping
43
44
45

if TYPE_CHECKING:
    from vllm.model_executor.layers.logits_processor import LogitsProcessor
46
    from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
47
    from vllm.model_executor.models.utils import WeightsMapper
48
49

logger = init_logger(__name__)
50

51
52
53
54
55
56
57
58
59
_GLOBAL_LORA_ID = 0


def get_lora_id():
    global _GLOBAL_LORA_ID
    _GLOBAL_LORA_ID += 1
    return _GLOBAL_LORA_ID


60
_all_lora_classes: set[type[BaseLayerWithLoRA]] = {
61
62
63
    VocabParallelEmbeddingWithLoRA,
    ColumnParallelLinearWithLoRA,
    MergedColumnParallelLinearWithLoRA,
64
65
    QKVParallelLinearWithLoRA,
    MergedQKVParallelLinearWithLoRA,
66
    RowParallelLinearWithLoRA,
67
    ReplicatedLinearWithLoRA,
68
69
    LogitsProcessorWithLoRA,
    ColumnParallelLinearWithShardedLoRA,
70
    QKVParallelLinearWithShardedLoRA,
71
    MergedColumnParallelLinearWithShardedLoRA,
72
    MergedQKVParallelLinearWithShardedLoRA,
73
    RowParallelLinearWithShardedLoRA,
74
    FusedMoEWithLoRA,
75
    FusedMoE3DWithLoRA,
76
77
78
}


79
80
81
82
83
84
85
86
def is_moe_model(model: nn.Module) -> bool:
    """Checks if the model contains FusedMoE layers and warns the user."""
    if any(isinstance(module, FusedMoE) for module in model.modules()):
        logger.info_once("MoE model detected. Using fused MoE LoRA implementation.")
        return True
    return False


87
88
89
90
91
def from_layer(
    layer: nn.Module,
    max_loras: int,
    lora_config: LoRAConfig,
    packed_modules_list: list,
92
    model_config: PretrainedConfig | None = None,
93
) -> nn.Module:
94
95
    for lora_cls in _all_lora_classes:
        # specifying kwargs so they can be easily accessed in decorator
96
97
98
99
100
101
        if lora_cls.can_replace_layer(
            source_layer=layer,
            lora_config=lora_config,
            packed_modules_list=packed_modules_list,
            model_config=model_config,
        ):
102
            instance_layer = lora_cls(layer)
103
            instance_layer.create_lora_weights(max_loras, lora_config, model_config)
104
            return instance_layer
105
106
107
108
    return layer


def from_layer_logits_processor(
109
110
    layer: "LogitsProcessor",
    lm_head: "ParallelLMHead",
111
112
    max_loras: int,
    lora_config: LoRAConfig,
113
    model_config: PretrainedConfig | None = None,
114
) -> LogitsProcessorWithLoRA:
115
116
117
118
119
120
121
    ret = LogitsProcessorWithLoRA(
        layer,
        lm_head.embedding_dim,
        lm_head.weight.dtype,
        lm_head.weight.device,
        lm_head.get_sharded_to_full_mapping(),
    )
122
123
124
    ret.create_lora_weights(max_loras, lora_config, model_config)
    return ret

125

126
127
128
def replace_submodule(
    model: nn.Module, module_name: str, new_module: nn.Module
) -> nn.Module:
129
130
131
132
133
134
135
    """Replace a submodule in a model with a new module."""
    parent = model.get_submodule(".".join(module_name.split(".")[:-1]))
    target_name = module_name.split(".")[-1]
    setattr(parent, target_name, new_module)
    return new_module


136
def parse_fine_tuned_lora_name(
137
    name: str, weights_mapper: Optional["WeightsMapper"] = None
138
) -> tuple[str, bool]:
139
140
141
142
143
    """Parse the name of lora weights.

    args:
        name: the name of the fine-tuned LoRA, e.g.
            base_model.model.dense1.weight
144
145
        weights_mapper: maps the name of weight, e.g.
            `model.` -> `language_model.model.`,
146
    return:
147
        tuple(module_name, is_lora_a):
148
149
150
            module_name: the name of the module, e.g. model.dense1,
            is_lora_a whether the tensor is lora_a or lora_b.
    """
151

152
    # LoRA weight qualified name usually starts with `base_model.model.`,
153
154
    # so we remove the prefix `base_model.model.` to make the following
    # mapping correctly.
155
    if name.startswith("base_model.model."):
156
157
158
159
        name = name.replace("base_model.model.", "")
        name = weights_mapper._map_name(name) if weights_mapper else name
        # recover the prefix `base_model.model.`
        name = "base_model.model." + name
160
161
    else:
        name = weights_mapper._map_name(name) if weights_mapper else name
162

163
164
165
    # In some situations, we may not start with `base_model.model.`.
    # If we don't (e.g., ibm-granite/granite-speech-3.3-8b),
    # we should keep the prefix intact.
166
    start_index = 2 if name.startswith("base_model.model.") else 0
167

168
    parts = name.split(".")
169
    if parts[-1] == "weight" and (parts[-2] == "lora_A" or parts[-2] == "lora_B"):
170
        new_name = ".".join(parts[start_index:-2])
171
        return new_name, parts[-2] == "lora_A"
172

173
    if parts[-1] == "lora_embedding_A" or parts[-1] == "lora_embedding_B":
174
        new_name = ".".join(parts[start_index:-1])
175
        return new_name, parts[-1] == "lora_embedding_A"
176

177
    raise ValueError(f"{name} is unsupported LoRA weight")
178
179


180
181
def is_base_embeddding_weights(name: str) -> bool:
    # hardcoded subfixes for input & output embedding weights
182
183
184
    embedding_suffixes = (
        ".embed_tokens.base_layer.weight",
        ".lm_head.base_layer.weight",
185
    )
186
    return name.endswith(embedding_suffixes)
187
188


189
def is_regex_target_modules(
190
    load_modules: str | list[str], expected_lora_modules: set[str]
191
) -> bool:
192
    """
193
194
195
    PEFT supports passing `target_modules` in the form of regular expressions,
    such as `model.*(q_proj|k_proj|v_proj)$`. This function is mainly used to
    determine whether the suffix in the regular expression is present in the
196
197
198
199
200
201
202
203
204
205
    `expected_lora_modules`.
    """

    def is_valid_regex(pattern):
        try:
            re.compile(pattern)
            return True
        except re.error:
            return False

206
207
    def is_subset(sub_list, full_set):
        return set(sub_list).issubset(full_set)
208
209
210
211
212
213
214
215
216
217
218
219
220
221

    # Similar to PEFT's processing logic, regex-related operations are only
    #  executed when the load_modules is a `str`.
    if not isinstance(load_modules, str):
        return False

    if is_valid_regex(load_modules):
        match = re.search(r"\((.*?)\)\$?$", load_modules)
        if match:
            suffix = match.group(1).split("|")
            return is_subset(suffix, expected_lora_modules)
    return False


222
def get_supported_lora_modules(model: nn.Module) -> list[str]:
223
224
225
    """
    In vLLM, all linear layers support LoRA.
    """
226

227
    supported_lora_modules: set[str] = set()
228
    for name, module in model.named_modules():
229
230
231
232
233
234
235
236
        # get the embedding modules if the module's embedding_modules
        # is not empty.
        embedding_modules = getattr(module, "embedding_modules", None)
        if embedding_modules is not None:
            for name in embedding_modules:
                supported_lora_modules.add(name)

        # get all the linear subfixes.
237
        if isinstance(module, (LinearBase,)):
238
            supported_lora_modules.add(name.split(".")[-1])
239

240
241
242
        if isinstance(module, (FusedMoE,)):
            supported_lora_modules.add(name.split(".")[-1])

243
244
245
    return list(supported_lora_modules)


246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
def get_adapter_absolute_path(lora_path: str) -> str:
    """
    Resolves the given lora_path to an absolute local path.

    If the lora_path is identified as a Hugging Face model identifier,
    it will download the model and return the local snapshot path.
    Otherwise, it treats the lora_path as a local file path and
    converts it to an absolute path.

    Parameters:
    lora_path (str): The path to the lora model, which can be an absolute path,
                     a relative path, or a Hugging Face model identifier.

    Returns:
    str: The resolved absolute local path to the lora model.
    """

    # Check if the path is an absolute path. Return it no matter exists or not.
    if os.path.isabs(lora_path):
        return lora_path

    # If the path starts with ~, expand the user home directory.
268
    if lora_path.startswith("~"):
269
270
271
272
273
274
275
276
        return os.path.expanduser(lora_path)

    # Check if the expanded relative path exists locally.
    if os.path.exists(lora_path):
        return os.path.abspath(lora_path)

    # If the path does not exist locally, assume it's a Hugging Face repo.
    try:
277
278
279
280
281
282
283
        local_snapshot_path = huggingface_hub.snapshot_download(repo_id=lora_path)
    except (
        HfHubHTTPError,
        RepositoryNotFoundError,
        EntryNotFoundError,
        HFValidationError,
    ):
284
        # Handle errors that may occur during the download
285
        # Return original path instead of throwing error here
286
287
288
289
        logger.exception("Error downloading the HuggingFace model")
        return lora_path

    return local_snapshot_path
290
291
292
293
294
295
296
297
298
299
300


def process_packed_modules_mapping(model: nn.Module) -> dict[str, list[str]]:
    if is_moe_model(model):
        if moe_packed_mapping := get_moe_expert_mapping(model):
            # This method generates and returns a dictionary mapping packed module
            # names to lists of their corresponding submodule names. It includes
            # both static mappings and dynamic mappings for expert layers, where
            # the expert indices are expanded based on the configured number
            # of routed experts.
            packed_modules_mapping = get_packed_modules_mapping(model)
301
            if not model.is_3d_moe_weight:
302
303
304
305
306
                # 3D MoE LoRA does not need `packed_modules_mapping`
                packed_modules_mapping["experts"] = [
                    weight_name.rstrip(".")
                    for _, weight_name, _, _ in moe_packed_mapping
                ]
307
308
309
310
311
312
313
314
315

            return packed_modules_mapping
        else:
            raise AttributeError(
                "To support LoRA for MoE model, "
                "'get_expert_mapping' must be implemented"
            )
    else:
        return get_packed_modules_mapping(model)