@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 67312cad11
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 11, 21:15 UTC

[Misc] Split the LoRA code (#30253)

Signed-off-by: Jee Jee Li <pandaleefree@gmail.com>

Jee Jee Li committed Dec 9, 2025 at 00:59 UTC 67312cad11835bd75ca55fda83708d4806b82436
9 files changed +265 -243
tests/lora/test_layers.py
+1 -1
@@ -28,7 +28,7 @@ from vllm.lora.layers import (
28 RowParallelLinearWithShardedLoRA,
29 VocabParallelEmbeddingWithLoRA,
30 )
31 -from vllm.lora.models import LoRALayerWeights, PackedLoRALayerWeights
31 +from vllm.lora.lora_weights import LoRALayerWeights, PackedLoRALayerWeights
32 from vllm.lora.punica_wrapper import get_punica_wrapper
33 from vllm.model_executor.layers.linear import (
34 ColumnParallelLinear,
tests/lora/test_lora_checkpoints.py
+1 -1
@@ -3,7 +3,7 @@
3
4 import pytest
5
6 -from vllm.lora.models import LoRAModel
6 +from vllm.lora.lora_model import LoRAModel
7 from vllm.lora.peft_helper import PEFTHelper
8 from vllm.model_executor.models.baichuan import BaiChuanBaseForCausalLM
9 from vllm.model_executor.models.utils import WeightsMapper
tests/lora/test_lora_huggingface.py
+1 -1
@@ -3,7 +3,7 @@
3
4 import pytest
5
6 -from vllm.lora.models import LoRAModel
6 +from vllm.lora.lora_model import LoRAModel
7 from vllm.lora.peft_helper import PEFTHelper
8 from vllm.lora.utils import get_adapter_absolute_path
9 from vllm.model_executor.models.qwen3 import Qwen3ForCausalLM
tests/lora/test_lora_manager.py
+2 -2
@@ -15,10 +15,10 @@ from vllm.lora.layers import (
15 MergedColumnParallelLinearWithLoRA,
16 RowParallelLinearWithLoRA,
17 )
18 +from vllm.lora.lora_model import LoRAModel
19 from vllm.lora.lora_weights import LoRALayerWeights, PackedLoRALayerWeights
19 -from vllm.lora.models import (
20 +from vllm.lora.model_manager import (
21 LoRAMapping,
21 - LoRAModel,
22 LoRAModelManager,
23 LRUCacheLoRAModelManager,
24 )
tests/lora/test_worker.py
+1 -1
@@ -16,7 +16,7 @@ from vllm.config import (
16 )
17 from vllm.config.load import LoadConfig
18 from vllm.config.lora import LoRAConfig
19 -from vllm.lora.models import LoRAMapping
19 +from vllm.lora.model_manager import LoRAMapping
20 from vllm.lora.request import LoRARequest
21 from vllm.v1.worker.gpu_worker import Worker
22
vllm/lora/lora_model.py new
+246
@@ -0,0 +1,246 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +import os
5 +
6 +import safetensors.torch
7 +import torch
8 +
9 +from vllm.logger import init_logger
10 +from vllm.lora.lora_weights import LoRALayerWeights
11 +from vllm.lora.peft_helper import PEFTHelper
12 +from vllm.lora.utils import (
13 + get_lora_id,
14 + is_base_embeddding_weights,
15 + is_regex_target_modules,
16 + parse_fine_tuned_lora_name,
17 +)
18 +from vllm.model_executor.model_loader.tensorizer import TensorizerConfig
19 +from vllm.model_executor.models.utils import WeightsMapper
20 +from vllm.utils.platform_utils import is_pin_memory_available
21 +
22 +logger = init_logger(__name__)
23 +
24 +
25 +class LoRAModel:
26 + """A LoRA fine-tuned model."""
27 +
28 + def __init__(
29 + self,
30 + lora_model_id: int,
31 + rank: int,
32 + loras: dict[str, LoRALayerWeights],
33 + ) -> None:
34 + """
35 + Args:
36 + lora_model_id: The integer id for the lora model.
37 + rank: lora rank.
38 + loras: module name -> weights for lora-replaced layers.
39 +
40 + """
41 + self.id = lora_model_id
42 +
43 + assert lora_model_id > 0, (
44 + f"a valid lora id should be greater than 0, got {self.id}"
45 + )
46 + self.rank = rank
47 + self.loras: dict[str, LoRALayerWeights] = loras
48 +
49 + def clone(self, lora_model_id: int) -> "LoRAModel":
50 + """Return a copy of the object with different ids.
51 +
52 + Will share the underlying tensors."""
53 + return self.__class__(
54 + lora_model_id,
55 + rank=self.rank,
56 + loras=self.loras.copy(),
57 + )
58 +
59 + def get_lora(self, module_name: str) -> LoRALayerWeights | None:
60 + """Get LoRA for a given module by name"""
61 + return self.loras.get(module_name, None)
62 +
63 + def check_lora_name(self, lora_name: str) -> bool:
64 + return lora_name in self.loras
65 +
66 + @classmethod
67 + def from_lora_tensors(
68 + cls,
69 + lora_model_id: int,
70 + tensors: dict[str, torch.Tensor],
71 + peft_helper: PEFTHelper,
72 + device: str = "cuda",
73 + dtype: torch.dtype | None = None,
74 + model_vocab_size: int | None = None,
75 + weights_mapper: WeightsMapper | None = None,
76 + ) -> "LoRAModel":
77 + """Create a LoRAModel from a dictionary of tensors."""
78 + pin_memory = str(device) == "cpu" and is_pin_memory_available()
79 + loras: dict[str, LoRALayerWeights] = {}
80 + for tensor_name, tensor in tensors.items():
81 + if is_base_embeddding_weights(tensor_name):
82 + continue
83 + module_name, is_lora_a = parse_fine_tuned_lora_name(
84 + tensor_name, weights_mapper
85 + )
86 + if module_name not in loras:
87 + loras[module_name] = LoRALayerWeights.from_config(
88 + module_name, peft_helper
89 + )
90 +
91 + if is_lora_a:
92 + if (
93 + "lora_embedding_A" in tensor_name
94 + and model_vocab_size is not None
95 + and model_vocab_size != tensor.shape[1]
96 + ):
97 + raise RuntimeError(
98 + f"The embedding LoRA size({tensor.shape[1]}) must be consistent"
99 + f" with the base model's vocabulary size({model_vocab_size})."
100 + )
101 + loras[module_name].lora_a = tensor.to(device=device, dtype=dtype)
102 + if pin_memory:
103 + loras[module_name].lora_a = loras[module_name].lora_a.pin_memory()
104 + else:
105 + loras[module_name].lora_b = tensor.to(device=device, dtype=dtype)
106 +
107 + if pin_memory:
108 + loras[module_name].lora_b = loras[module_name].lora_b.pin_memory()
109 +
110 + return cls(lora_model_id, peft_helper.r, loras)
111 +
112 + @classmethod
113 + def from_local_checkpoint(
114 + cls,
115 + lora_dir: str,
116 + expected_lora_modules: set[str],
117 + peft_helper: PEFTHelper,
118 + *,
119 + lora_model_id: int | None = None,
120 + device: str = "cuda",
121 + dtype: torch.dtype | None = None,
122 + model_vocab_size: int | None = None,
123 + weights_mapper: WeightsMapper | None = None,
124 + tensorizer_config_dict: dict | None = None,
125 + ) -> "LoRAModel":
126 + """Create a LoRAModel from a local checkpoint.
127 +
128 + Args:
129 + lora_dir: The local path that has lora data.
130 + expected_lora_modules: Name of modules that are expected to be
131 + replaced by lora.
132 + peft_helper: Loaded lora configuration information.
133 + lora_model_id: LoRA model id. If not given, automatically set by
134 + a global counter.
135 + device: Device where the lora model is loaded.
136 + dtype: dtype of the lora model weights.
137 +
138 + Returns:
139 + Loaded LoRA Model.
140 + """
141 + lora_tensor_path = os.path.join(lora_dir, "adapter_model.safetensors")
142 + lora_bin_file_path = os.path.join(lora_dir, "adapter_model.bin")
143 + lora_pt_file_path = os.path.join(lora_dir, "adapter_model.pt")
144 +
145 + tensors: dict[str, torch.Tensor] = {}
146 + unexpected_modules: list[list[str] | str] = []
147 +
148 + def check_unexpected_modules(modules: dict):
149 + for lora_module in modules.keys(): # noqa
150 + if is_base_embeddding_weights(lora_module):
151 + continue
152 + # Handle PEFT file format where experts.base_layer is the
153 + # gate_up_proj and experts is the down_proj
154 + if "base_layer" in lora_module:
155 + continue
156 + module_name, _ = parse_fine_tuned_lora_name(lora_module, weights_mapper)
157 + # Case for expert lora weights
158 + if ".experts" in module_name:
159 + expert_idx = module_name.find(".experts")
160 + expert_suffix = module_name[expert_idx + 1 :]
161 + if expert_suffix not in expected_lora_modules:
162 + unexpected_modules.append(module_name)
163 +
164 + elif module_name.rsplit(".", 1)[-1] not in expected_lora_modules:
165 + unexpected_modules.append(module_name)
166 +
167 + if unexpected_modules:
168 + raise ValueError(
169 + f"While loading {lora_dir}, expected"
170 + f" target modules in {expected_lora_modules}"
171 + f" but received {unexpected_modules}."
172 + f" Please verify that the loaded LoRA module is correct"
173 + )
174 +
175 + if tensorizer_config_dict:
176 + from tensorizer import TensorDeserializer
177 +
178 + tensorizer_config = TensorizerConfig(**tensorizer_config_dict)
179 + lora_tensor_path = os.path.join(
180 + tensorizer_config.tensorizer_dir, "adapter_model.tensors"
181 + )
182 + tensorizer_args = tensorizer_config._construct_tensorizer_args()
183 + tensors = TensorDeserializer(
184 + lora_tensor_path,
185 + dtype=tensorizer_config.dtype,
186 + **tensorizer_args.deserialization_kwargs,
187 + )
188 + check_unexpected_modules(tensors)
189 +
190 + elif os.path.isfile(lora_tensor_path):
191 + # Find unexpected modules.
192 + # Use safetensor key as a source of truth to find expected modules.
193 + # in peft if you have target_modules A, B, C and C does not exist
194 + # in the model it won’t error and model will be trained with A, B
195 + # loraified. C won’t exist in the safetensor but it will exist in
196 + # the target_modules of the adapter_config.json.
197 + unexpected_modules = []
198 + with safetensors.safe_open(lora_tensor_path, framework="pt") as f: # type: ignore
199 + # Load tensors if there are only expected modules.
200 + check_unexpected_modules(f)
201 + for module in f.keys(): # noqa
202 + tensors[module] = f.get_tensor(module)
203 + elif os.path.isfile(lora_bin_file_path) or os.path.isfile(lora_pt_file_path):
204 + # When a bin/pt file is provided, we rely on config to find
205 + # unexpected modules.
206 + unexpected_modules = []
207 + target_modules = peft_helper.target_modules
208 + if not isinstance(target_modules, list):
209 + target_modules = [target_modules]
210 + for module in target_modules:
211 + # Compatible with more modules,
212 + # such as:layers.11.self_attn.k_proj
213 + part_name = module.split(".")[-1]
214 + if part_name not in expected_lora_modules:
215 + unexpected_modules.append(module)
216 + # loaded lora's target modules must be a subset of
217 + # expected_lora_modules. It is not reliable. See
218 + # https://github.com/vllm-project/vllm/pull/5909. But there's no
219 + # other better mechanism.
220 + if unexpected_modules and not is_regex_target_modules(
221 + peft_helper.target_modules, expected_lora_modules
222 + ):
223 + raise ValueError(
224 + f"While loading {lora_dir}, expected"
225 + f" target modules in {expected_lora_modules}"
226 + f" but received {unexpected_modules}."
227 + f" Please verify that the loaded LoRA module is correct"
228 + )
229 + lora_file_path = (
230 + lora_bin_file_path
231 + if os.path.isfile(lora_bin_file_path)
232 + else lora_pt_file_path
233 + )
234 + tensors = torch.load(lora_file_path, map_location=device, weights_only=True)
235 + else:
236 + raise ValueError(f"{lora_dir} doesn't contain tensors")
237 +
238 + return cls.from_lora_tensors(
239 + lora_model_id=get_lora_id() if lora_model_id is None else lora_model_id,
240 + tensors=tensors,
241 + peft_helper=peft_helper,
242 + device=device,
243 + dtype=dtype,
244 + model_vocab_size=model_vocab_size,
245 + weights_mapper=weights_mapper,
246 + )
vllm/lora/model_manager.py renamed
+2 -235
@@ -2,38 +2,32 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import math
5 -import os
5 from collections.abc import Callable
6 from typing import TypeVar
7
8 import regex as re
10 -import safetensors.torch
9 import torch
10 from torch import nn
11
12 from vllm.config.lora import LoRAConfig
13 from vllm.logger import init_logger
14 from vllm.lora.layers import BaseLayerWithLoRA, FusedMoE3DWithLoRA, LoRAMapping
15 +from vllm.lora.lora_model import LoRAModel
16 from vllm.lora.lora_weights import LoRALayerWeights, PackedLoRALayerWeights
18 -from vllm.lora.peft_helper import PEFTHelper
17 from vllm.lora.punica_wrapper import get_punica_wrapper
18 from vllm.lora.utils import (
19 from_layer,
20 from_layer_logits_processor,
21 get_supported_lora_modules,
24 - is_base_embeddding_weights,
22 is_moe_model,
26 - is_regex_target_modules,
27 - parse_fine_tuned_lora_name,
23 process_packed_modules_mapping,
24 replace_submodule,
25 )
26 from vllm.model_executor.layers.fused_moe import FusedMoE
32 -from vllm.model_executor.model_loader.tensorizer import TensorizerConfig
27 from vllm.model_executor.models import SupportsLoRA, supports_multimodal
28 from vllm.model_executor.models.interfaces import is_pooling_model
29 from vllm.model_executor.models.module_mapping import MultiModelKeys
36 -from vllm.model_executor.models.utils import PPMissingLayer, WeightsMapper
30 +from vllm.model_executor.models.utils import PPMissingLayer
31 from vllm.utils.cache import LRUCache
32 from vllm.utils.platform_utils import is_pin_memory_available
33
@@ -53,233 +47,6 @@ class AdapterLRUCache(LRUCache[int, T]):
47 return super()._on_remove(key, value)
48
49
56 -_GLOBAL_LORA_ID = 0
57 -
58 -
59 -def get_lora_id():
60 - global _GLOBAL_LORA_ID
61 - _GLOBAL_LORA_ID += 1
62 - return _GLOBAL_LORA_ID
63 -
64 -
65 -class LoRAModel:
66 - """A LoRA fine-tuned model."""
67 -
68 - def __init__(
69 - self,
70 - lora_model_id: int,
71 - rank: int,
72 - loras: dict[str, LoRALayerWeights],
73 - ) -> None:
74 - """
75 - Args:
76 - lora_model_id: The integer id for the lora model.
77 - rank: lora rank.
78 - loras: module name -> weights for lora-replaced layers.
79 -
80 - """
81 - self.id = lora_model_id
82 -
83 - assert lora_model_id > 0, (
84 - f"a valid lora id should be greater than 0, got {self.id}"
85 - )
86 - self.rank = rank
87 - self.loras: dict[str, LoRALayerWeights] = loras
88 -
89 - def clone(self, lora_model_id: int) -> "LoRAModel":
90 - """Return a copy of the object with different ids.
91 -
92 - Will share the underlying tensors."""
93 - return self.__class__(
94 - lora_model_id,
95 - rank=self.rank,
96 - loras=self.loras.copy(),
97 - )
98 -
99 - def get_lora(self, module_name: str) -> LoRALayerWeights | None:
100 - """Get LoRA for a given module by name"""
101 - return self.loras.get(module_name, None)
102 -
103 - def check_lora_name(self, lora_name: str) -> bool:
104 - return lora_name in self.loras
105 -
106 - @classmethod
107 - def from_lora_tensors(
108 - cls,
109 - lora_model_id: int,
110 - tensors: dict[str, torch.Tensor],
111 - peft_helper: PEFTHelper,
112 - device: str = "cuda",
113 - dtype: torch.dtype | None = None,
114 - model_vocab_size: int | None = None,
115 - weights_mapper: WeightsMapper | None = None,
116 - ) -> "LoRAModel":
117 - """Create a LoRAModel from a dictionary of tensors."""
118 -
119 - loras: dict[str, LoRALayerWeights] = {}
120 - for tensor_name, tensor in tensors.items():
121 - if is_base_embeddding_weights(tensor_name):
122 - continue
123 - module_name, is_lora_a = parse_fine_tuned_lora_name(
124 - tensor_name, weights_mapper
125 - )
126 - if module_name not in loras:
127 - loras[module_name] = LoRALayerWeights.from_config(
128 - module_name, peft_helper
129 - )
130 -
131 - if is_lora_a:
132 - if (
133 - "lora_embedding_A" in tensor_name
134 - and model_vocab_size is not None
135 - and model_vocab_size != tensor.shape[1]
136 - ):
137 - raise RuntimeError(
138 - f"The embedding LoRA size({tensor.shape[1]}) must be consistent"
139 - f" with the base model's vocabulary size({model_vocab_size})."
140 - )
141 - loras[module_name].lora_a = tensor.to(device=device, dtype=dtype)
142 - else:
143 - loras[module_name].lora_b = tensor.to(device=device, dtype=dtype)
144 - return cls(lora_model_id, peft_helper.r, loras)
145 -
146 - @classmethod
147 - def from_local_checkpoint(
148 - cls,
149 - lora_dir: str,
150 - expected_lora_modules: set[str],
151 - peft_helper: PEFTHelper,
152 - *,
153 - lora_model_id: int | None = None,
154 - device: str = "cuda",
155 - dtype: torch.dtype | None = None,
156 - model_vocab_size: int | None = None,
157 - weights_mapper: WeightsMapper | None = None,
158 - tensorizer_config_dict: dict | None = None,
159 - ) -> "LoRAModel":
160 - """Create a LoRAModel from a local checkpoint.
161 -
162 - Args:
163 - lora_dir: The local path that has lora data.
164 - expected_lora_modules: Name of modules that are expected to be
165 - replaced by lora.
166 - peft_helper: Loaded lora configuration information.
167 - lora_model_id: LoRA model id. If not given, automatically set by
168 - a global counter.
169 - device: Device where the lora model is loaded.
170 - dtype: dtype of the lora model weights.
171 -
172 - Returns:
173 - Loaded LoRA Model.
174 - """
175 - lora_tensor_path = os.path.join(lora_dir, "adapter_model.safetensors")
176 - lora_bin_file_path = os.path.join(lora_dir, "adapter_model.bin")
177 - lora_pt_file_path = os.path.join(lora_dir, "adapter_model.pt")
178 -
179 - tensors: dict[str, torch.Tensor] = {}
180 - unexpected_modules: list[list[str] | str] = []
181 -
182 - def check_unexpected_modules(modules: dict):
183 - for lora_module in modules.keys(): # noqa
184 - if is_base_embeddding_weights(lora_module):
185 - continue
186 - # Handle PEFT file format where experts.base_layer is the
187 - # gate_up_proj and experts is the down_proj
188 - if "base_layer" in lora_module:
189 - continue
190 - module_name, _ = parse_fine_tuned_lora_name(lora_module, weights_mapper)
191 - # Case for expert lora weights
192 - if ".experts" in module_name:
193 - expert_idx = module_name.find(".experts")
194 - expert_suffix = module_name[expert_idx + 1 :]
195 - if expert_suffix not in expected_lora_modules:
196 - unexpected_modules.append(module_name)
197 -
198 - elif module_name.rsplit(".", 1)[-1] not in expected_lora_modules:
199 - unexpected_modules.append(module_name)
200 -
201 - if unexpected_modules:
202 - raise ValueError(
203 - f"While loading {lora_dir}, expected"
204 - f" target modules in {expected_lora_modules}"
205 - f" but received {unexpected_modules}."
206 - f" Please verify that the loaded LoRA module is correct"
207 - )
208 -
209 - if tensorizer_config_dict:
210 - from tensorizer import TensorDeserializer
211 -
212 - tensorizer_config = TensorizerConfig(**tensorizer_config_dict)
213 - lora_tensor_path = os.path.join(
214 - tensorizer_config.tensorizer_dir, "adapter_model.tensors"
215 - )
216 - tensorizer_args = tensorizer_config._construct_tensorizer_args()
217 - tensors = TensorDeserializer(
218 - lora_tensor_path,
219 - dtype=tensorizer_config.dtype,
220 - **tensorizer_args.deserialization_kwargs,
221 - )
222 - check_unexpected_modules(tensors)
223 -
224 - elif os.path.isfile(lora_tensor_path):
225 - # Find unexpected modules.
226 - # Use safetensor key as a source of truth to find expected modules.
227 - # in peft if you have target_modules A, B, C and C does not exist
228 - # in the model it won’t error and model will be trained with A, B
229 - # loraified. C won’t exist in the safetensor but it will exist in
230 - # the target_modules of the adapter_config.json.
231 - unexpected_modules = []
232 - with safetensors.safe_open(lora_tensor_path, framework="pt") as f: # type: ignore
233 - # Load tensors if there are only expected modules.
234 - check_unexpected_modules(f)
235 - for module in f.keys(): # noqa
236 - tensors[module] = f.get_tensor(module)
237 - elif os.path.isfile(lora_bin_file_path) or os.path.isfile(lora_pt_file_path):
238 - # When a bin/pt file is provided, we rely on config to find
239 - # unexpected modules.
240 - unexpected_modules = []
241 - target_modules = peft_helper.target_modules
242 - if not isinstance(target_modules, list):
243 - target_modules = [target_modules]
244 - for module in target_modules:
245 - # Compatible with more modules,
246 - # such as:layers.11.self_attn.k_proj
247 - part_name = module.split(".")[-1]
248 - if part_name not in expected_lora_modules:
249 - unexpected_modules.append(module)
250 - # loaded lora's target modules must be a subset of
251 - # expected_lora_modules. It is not reliable. See
252 - # https://github.com/vllm-project/vllm/pull/5909. But there's no
253 - # other better mechanism.
254 - if unexpected_modules and not is_regex_target_modules(
255 - peft_helper.target_modules, expected_lora_modules
256 - ):
257 - raise ValueError(
258 - f"While loading {lora_dir}, expected"
259 - f" target modules in {expected_lora_modules}"
260 - f" but received {unexpected_modules}."
261 - f" Please verify that the loaded LoRA module is correct"
262 - )
263 - lora_file_path = (
264 - lora_bin_file_path
265 - if os.path.isfile(lora_bin_file_path)
266 - else lora_pt_file_path
267 - )
268 - tensors = torch.load(lora_file_path, map_location=device, weights_only=True)
269 - else:
270 - raise ValueError(f"{lora_dir} doesn't contain tensors")
271 -
272 - return cls.from_lora_tensors(
273 - lora_model_id=get_lora_id() if lora_model_id is None else lora_model_id,
274 - tensors=tensors,
275 - peft_helper=peft_helper,
276 - device=device,
277 - dtype=dtype,
278 - model_vocab_size=model_vocab_size,
279 - weights_mapper=weights_mapper,
280 - )
281 -
282 -
50 class LoRAModelManager:
51 """A manager that manages multiple LoRA-fine-tuned models."""
52
vllm/lora/utils.py
+9
@@ -48,6 +48,15 @@ if TYPE_CHECKING:
48
49 logger = init_logger(__name__)
50
51 +_GLOBAL_LORA_ID = 0
52 +
53 +
54 +def get_lora_id():
55 + global _GLOBAL_LORA_ID
56 + _GLOBAL_LORA_ID += 1
57 + return _GLOBAL_LORA_ID
58 +
59 +
60 _all_lora_classes: set[type[BaseLayerWithLoRA]] = {
61 VocabParallelEmbeddingWithLoRA,
62 ColumnParallelLinearWithLoRA,
vllm/lora/worker_manager.py
+2 -2
@@ -8,8 +8,8 @@ import torch
8
9 from vllm.config import VllmConfig
10 from vllm.logger import init_logger
11 -from vllm.lora.models import (
12 - LoRAModel,
11 +from vllm.lora.lora_model import LoRAModel
12 +from vllm.lora.model_manager import (
13 LoRAModelManager,
14 LRUCacheLoRAModelManager,
15 create_lora_manager,