main
py 300 lines 11.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Define LoRA functionality mixin for model runners."""
4
5 from collections.abc import Callable
6 from contextlib import contextmanager
7
8 import numpy as np
9 import torch
10 import torch.nn as nn
11
12 from vllm.config import VllmConfig
13 from vllm.config.lora import LoRAConfig
14 from vllm.logger import init_logger
15 from vllm.lora.layers import LoRAMapping, LoRAMappingType
16 from vllm.lora.request import LoRARequest
17 from vllm.lora.worker_manager import LRUCacheWorkerLoRAManager
18 from vllm.model_executor.models import supports_lora
19 from vllm.v1.worker.gpu_input_batch import InputBatch
20
21 logger = init_logger(__name__)
22
23
24 # Defined as a mixin for GPUModelRunner
25 class LoRAModelRunnerMixin:
26 lora_config: LoRAConfig | None
27 get_model: Callable[[], nn.Module]
28
29 def reset_lora_state(self) -> None:
30 """Invalidate LoRA state after base weights are replaced."""
31 if not self.lora_config:
32 return
33
34 from vllm.lora.layers.logits_processor import LogitsProcessorWithLoRA
35
36 self.lora_manager.remove_all_adapters()
37 for module in self.get_model().modules():
38 if isinstance(module, LogitsProcessorWithLoRA):
39 module.reset_sharded_to_full_mapping()
40
41 def load_lora_model(
42 self,
43 model: nn.Module,
44 vllm_config: VllmConfig,
45 device: torch.device,
46 ) -> nn.Module:
47 if not supports_lora(model):
48 raise ValueError(f"{model.__class__.__name__} does not support LoRA yet.")
49
50 # Add LoRA Manager to the Model Runner
51 self.lora_manager = LRUCacheWorkerLoRAManager(
52 vllm_config,
53 device,
54 model.embedding_modules,
55 )
56 return self.lora_manager.create_lora_manager(model, vllm_config)
57
58 def _set_active_loras(
59 self,
60 prompt_lora_mapping: tuple[int, ...],
61 token_lora_mapping: tuple[int, ...],
62 lora_requests: set[LoRARequest],
63 mapping_type: LoRAMappingType = LoRAMappingType.LANGUAGE,
64 ) -> None:
65 self._ensure_lora_enabled()
66
67 # Set is_prefill to True, so we always use the SGMV kernels on
68 # non-cuda platforms.
69 # On cuda platforms we use the same kernels for prefill and
70 # decode and this flag is generally ignored.
71 lora_mapping = LoRAMapping(
72 token_lora_mapping,
73 prompt_lora_mapping,
74 is_prefill=True,
75 type=mapping_type,
76 )
77 self.lora_manager.set_active_adapters(lora_requests, lora_mapping)
78
79 def _ensure_lora_enabled(self) -> None:
80 if not hasattr(self, "lora_manager"):
81 raise RuntimeError("LoRA is not enabled. Use --enable-lora to enable LoRA.")
82
83 def set_active_loras(
84 self,
85 input_batch: InputBatch,
86 num_scheduled_tokens: np.ndarray,
87 num_sampled_tokens: np.ndarray | None = None,
88 mapping_type: LoRAMappingType = LoRAMappingType.LANGUAGE,
89 ) -> None:
90 if num_sampled_tokens is None:
91 num_sampled_tokens = np.ones_like(num_scheduled_tokens, dtype=np.int32)
92
93 prompt_lora_mapping: tuple[int, ...] # of size np.sum(num_sampled_tokens)
94 token_lora_mapping: tuple[int, ...] # of size np.sum(num_scheduled_tokens)
95 lora_requests: set[LoRARequest]
96 prompt_lora_mapping, token_lora_mapping, lora_requests = (
97 input_batch.make_lora_inputs(num_scheduled_tokens, num_sampled_tokens)
98 )
99 return self._set_active_loras(
100 prompt_lora_mapping, token_lora_mapping, lora_requests, mapping_type
101 )
102
103 @contextmanager
104 def maybe_setup_dummy_loras(
105 self, lora_config: LoRAConfig | None, remove_lora: bool = True
106 ):
107 if lora_config is None:
108 yield
109 else:
110 # __enter__ code
111 assert self.lora_manager is not None, "LoRA is not enabled"
112
113 num_loras = lora_config.max_loras
114 lora_warmup_rank: int = (
115 lora_config.max_lora_rank if lora_config.max_lora_rank < 8 else 8
116 )
117 lora_warmup_rank = self.lora_manager.get_dummy_lora_warmup_rank(
118 lora_warmup_rank
119 )
120 # Make dummy lora requests
121 lora_requests: set[LoRARequest] = {
122 LoRARequest(
123 lora_name=f"warmup_{lora_id}",
124 lora_int_id=lora_id,
125 lora_path="/not/a/real/path",
126 )
127 for lora_id in range(1, num_loras + 1)
128 }
129
130 with self.lora_manager.dummy_lora_cache():
131 # Add the dummy LoRAs here so _set_active_loras doesn't try to
132 # load from disk.
133 for lr in lora_requests:
134 self.lora_manager.add_dummy_lora(lr, rank=lora_warmup_rank)
135
136 yield
137
138 # __exit__ code
139 if remove_lora:
140 self.lora_manager.remove_all_adapters()
141
142 @contextmanager
143 def maybe_select_dummy_loras(
144 self,
145 lora_config: LoRAConfig | None,
146 num_scheduled_tokens: np.ndarray,
147 mapping_type: LoRAMappingType = LoRAMappingType.LANGUAGE,
148 num_sampled_tokens: np.ndarray | None = None,
149 num_active_loras: int = 0,
150 ):
151 """Context manager to select dummy LoRAs for capture/warmup.
152
153 Args:
154 lora_config: LoRA configuration, or None if LoRA is disabled.
155 num_scheduled_tokens: Array of scheduled token counts per request.
156 mapping_type: Which LoRA mapping to build (language or encoder).
157 num_sampled_tokens: Array of sampled token counts per request.
158 num_active_loras: Number of distinct active LoRAs to use.
159 - 0: No LoRA active (set up zero mappings).
160 - >0: Use exactly this many distinct LoRAs.
161
162 """
163 if num_sampled_tokens is None:
164 num_sampled_tokens = np.ones_like(num_scheduled_tokens, dtype=np.int32)
165
166 # Skip LoRA setup entirely only if no LoRA config
167 if lora_config is None:
168 yield
169 else:
170 # __enter__ code
171 assert self.lora_manager is not None, "LoRA is not enabled"
172
173 num_reqs = len(num_scheduled_tokens)
174 max_loras = lora_config.max_loras
175
176 # Determine how many distinct LoRAs to use and whether to include
177 # no-LoRA tokens (-1 entries).
178 # When num_active_loras > max_loras (e.g., max_loras + 1), we need
179 # to include -1 entries to simulate batches with both LoRA and
180 # no-LoRA tokens. This ensures prepare_tensors computes the correct
181 # num_active_loras that matches the cudagraph capture key.
182 if num_active_loras == 0:
183 # No LoRA active - use 0 mappings like the original code
184 effective_num_loras = 0
185 include_no_lora = False
186 elif num_active_loras > max_loras:
187 # num_active_loras > max_loras means we want max_loras adapters
188 # PLUS no-LoRA tokens (-1). This is the max_loras + 1 case.
189 effective_num_loras = max_loras
190 include_no_lora = True
191 else:
192 # Specific number of active LoRAs requested
193 effective_num_loras = min(num_active_loras, max_loras)
194 include_no_lora = False
195
196 # Make prompt lora mapping
197 # Assign LoRA IDs cyclically to simulate a worst-case scenario.
198 # LoRA IDs are 1-indexed (1 to max_loras) as required by LoRARequest.
199 # convert_mapping() will convert these to 0-indexed slot indices.
200 if effective_num_loras > 0:
201 if include_no_lora:
202 # Include -1 (no-LoRA) entries by cycling through
203 # -1, 1, 2, ..., effective_num_loras
204 # This ensures prepare_tensors sees both LoRA and no-LoRA
205 # tokens, computing num_active_loras = effective_num_loras+1
206 cycle_values = np.array(
207 list(range(1, effective_num_loras + 1)),
208 dtype=np.int32,
209 )
210 prompt_lora_mapping = cycle_values[
211 np.arange(num_reqs, dtype=np.int32) % len(cycle_values)
212 ]
213 else:
214 # Use 1 to effective_num_loras (1-indexed lora IDs)
215 prompt_lora_mapping = (
216 np.arange(num_reqs, dtype=np.int32) % effective_num_loras
217 ) + 1
218 else:
219 # No LoRA active - use 0 for all tokens (original behavior)
220 prompt_lora_mapping = np.zeros(num_reqs, dtype=np.int32)
221
222 # Make sample lora mapping
223 sample_lora_mapping = np.repeat(prompt_lora_mapping, num_sampled_tokens)
224
225 # Make token lora mapping
226 token_lora_mapping = np.repeat(prompt_lora_mapping, num_scheduled_tokens)
227
228 # Make dummy lora requests (only for the active LoRAs)
229 lora_requests: set[LoRARequest] = {
230 LoRARequest(
231 lora_name=f"warmup_{lora_id}",
232 lora_int_id=lora_id,
233 lora_path="/not/a/real/path",
234 )
235 for lora_id in range(1, effective_num_loras + 1)
236 }
237
238 self._set_active_loras(
239 tuple(sample_lora_mapping),
240 tuple(token_lora_mapping),
241 lora_requests,
242 mapping_type,
243 )
244
245 yield
246
247 @contextmanager
248 def maybe_dummy_run_with_lora(
249 self,
250 lora_config: LoRAConfig | None,
251 num_scheduled_tokens: np.ndarray,
252 num_sampled_tokens: np.ndarray,
253 remove_lora: bool = True,
254 num_active_loras: int = 0,
255 mapping_type: LoRAMappingType = LoRAMappingType.LANGUAGE,
256 ):
257 """Context manager for dummy runs with LoRA.
258
259 Args:
260 lora_config: LoRA configuration.
261 num_scheduled_tokens: Array of scheduled token counts per request.
262 num_sampled_tokens: Array of sampled token counts per request.
263 remove_lora: Whether to remove LoRAs after the context exits.
264 num_active_loras: Number of distinct active LoRAs to use.
265 LoRA is activated when num_active_loras > 0.
266 mapping_type: Which LoRA mapping to build (language or encoder).
267
268 """
269 with (
270 self.maybe_setup_dummy_loras(lora_config, remove_lora),
271 self.maybe_select_dummy_loras(
272 lora_config,
273 num_scheduled_tokens,
274 mapping_type,
275 num_sampled_tokens,
276 num_active_loras,
277 ),
278 ):
279 yield
280
281 def maybe_remove_all_loras(self, lora_config: LoRAConfig | None):
282 if lora_config is None:
283 return
284 self.lora_manager.remove_all_adapters()
285
286 def add_lora(self, lora_request: LoRARequest) -> bool:
287 self._ensure_lora_enabled()
288 return self.lora_manager.add_adapter(lora_request)
289
290 def remove_lora(self, lora_id: int) -> bool:
291 self._ensure_lora_enabled()
292 return self.lora_manager.remove_adapter(lora_id)
293
294 def pin_lora(self, lora_id: int) -> bool:
295 self._ensure_lora_enabled()
296 return self.lora_manager.pin_adapter(lora_id)
297
298 def list_loras(self) -> set[int]:
299 self._ensure_lora_enabled()
300 return self.lora_manager.list_adapters()