@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 6c663dfd5e
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 15, 17:15 UTC

[misc] LoRA - Skip LoRA kernels when not required (#15152)

Signed-off-by: Varun Sundar Rabindranath <varun@neuralmagic.com> Co-authored-by: Varun Sundar Rabindranath <varun@neuralmagic.com>

Varun Sundar Rabindranath committed Mar 25, 2025 at 20:33 UTC 6c663dfd5e5b5ab4a1eb46391c2c65d1eff0218f
4 files changed +113 -33
vllm/lora/ops/triton_ops/lora_expand.py
+12 -1
@@ -136,6 +136,7 @@ def _lora_expand(
136 num_tokens_per_lora: torch.Tensor, # shape [max-loras + 1]
137 lora_token_start_loc: torch.Tensor, # shape [max-loras + 2]
138 lora_ids: torch.Tensor, # shape [max-loras + 1]
139 + no_lora_flag_cpu: torch.Tensor, # shape [1]
140 offset_start: int = 0,
141 add_inputs: bool = False,
142 ) -> None:
@@ -157,11 +158,19 @@ def _lora_expand(
158 identifies the the region in token_indices_sorted_by_lora_ids that
159 LoRA lora_ids[i] should process.
160 lora_ids (torch.Tensor): LoRA ids to process.
161 + no_lora_flag_cpu (torch.Tensor): A CPU tensor of size 1, that indicates
162 + if there are any requests that require LoRA.
163 offset_start (int, optional): Offset start for output_tensor.
164 Defaults to 0.
165 add_inputs (bool, optional): Whether to add the input tensor to the
166 output tensor. Defaults to False.
167 """
168 +
169 + assert no_lora_flag_cpu.numel() == 1
170 + if no_lora_flag_cpu.item():
171 + # None of the inputs require LoRA.
172 + return
173 +
174 assert inputs.dtype in [torch.float16, torch.bfloat16, torch.float32]
175 for weight in lora_b_weights:
176 assert weight.dtype in [torch.float16, torch.bfloat16]
@@ -170,6 +179,8 @@ def _lora_expand(
179 assert output_tensor.is_contiguous()
180
181 # metadata sanity check.
182 + M = inputs.size(1)
183 + assert token_lora_mapping.size(0) == M
184 assert token_lora_mapping.size(0) == token_indices_sorted_by_lora_ids.size(
185 0)
186 assert lora_ids.size(0) == num_tokens_per_lora.size(0)
@@ -181,7 +192,6 @@ def _lora_expand(
192 inputs.device)
193
194 K = lora_b_weights[0].shape[-1] # K= rank
184 - M = inputs.size(1)
195 ADD_INPUTS = add_inputs
196 MAX_LORAS = lora_ids.size(0)
197 CAST_TYPE = False
@@ -263,6 +273,7 @@ def _lora_expand_fake(
273 num_tokens_per_lora: torch.Tensor,
274 lora_token_start_loc: torch.Tensor,
275 lora_ids: torch.Tensor,
276 + no_lora_flag_cpu: torch.Tensor,
277 offset_start: int = 0,
278 add_inputs: bool = False,
279 ) -> None:
vllm/lora/ops/triton_ops/lora_kernel_metadata.py
+36 -6
@@ -17,6 +17,17 @@ class LoRAKernelMeta:
17 num_tokens_per_lora: torch.Tensor
18 lora_token_start_loc: torch.Tensor
19
20 + # The V1 architecture uses the traced torch.compile graphs to execute
21 + # a forward pass. Things to note about this process,
22 + # 1. The tracing infers all python scalar datatype objects into a constant
23 + # value.
24 + # 2. The tracing cannot handle dynamic control flow. (dynamic control flow
25 + # is an experimental feature in pytorch)
26 + # 3. The internals of torch.ops functions are not traced.
27 + # We disguise the "no_lora" flag as a cpu tensor and leverage point number 3
28 + # to early exit from inside the lora_expand / lora_shrink torch operation.
29 + no_lora_flag_cpu: torch.Tensor
30 +
31 @staticmethod
32 def make(max_loras: int, max_num_tokens: int,
33 device: Union[torch.device, str]) -> "LoRAKernelMeta":
@@ -47,17 +58,24 @@ class LoRAKernelMeta:
58 lora_token_start_loc = torch.zeros(max_loras + 2,
59 dtype=torch.int32,
60 device=device)
61 +
62 + no_lora_flag_cpu = torch.tensor([False],
63 + dtype=torch.bool,
64 + device='cpu')
65 +
66 return LoRAKernelMeta(
67 token_lora_mapping=token_lora_mapping,
68 token_indices_sorted_by_lora_ids=token_indices_sorted_by_lora_ids,
69 active_lora_ids=active_lora_ids,
70 num_tokens_per_lora=num_tokens_per_lora,
55 - lora_token_start_loc=lora_token_start_loc)
71 + lora_token_start_loc=lora_token_start_loc,
72 + no_lora_flag_cpu=no_lora_flag_cpu)
73
74 def _reset(self):
75 self.active_lora_ids.fill_(-1)
76 self.num_tokens_per_lora.fill_(0)
77 self.lora_token_start_loc.fill_(0)
78 + self.no_lora_flag_cpu.fill_(False)
79
80 def prepare_tensors(self, token_lora_mapping: torch.Tensor) -> None:
81 """
@@ -70,6 +88,14 @@ class LoRAKernelMeta:
88
89 self._reset()
90
91 + # Check and record no-lora case.
92 + no_lora = torch.all(token_lora_mapping == -1)
93 + self.no_lora_flag_cpu[0] = no_lora
94 +
95 + if no_lora:
96 + # Early exit. LoRA kernels will not be run.
97 + return
98 +
99 num_tokens = token_lora_mapping.size(0)
100
101 # copy token lora mapping
@@ -100,7 +126,7 @@ class LoRAKernelMeta:
126 def meta_args(
127 self, token_nums: int
128 ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor,
103 - torch.Tensor]:
129 + torch.Tensor, torch.Tensor]:
130 """
131 This function returns the kernel metadata required for the current
132 forward pass execution of the kernel. The function returns all the
@@ -111,7 +137,11 @@ class LoRAKernelMeta:
137 token_nums (int): Number of input tokens in the current forward
138 pass.
139 """
114 - return (self.token_lora_mapping[:token_nums],
115 - self.token_indices_sorted_by_lora_ids[:token_nums],
116 - self.num_tokens_per_lora, self.lora_token_start_loc,
117 - self.active_lora_ids)
140 + return (
141 + self.token_lora_mapping[:token_nums],
142 + self.token_indices_sorted_by_lora_ids[:token_nums],
143 + self.num_tokens_per_lora,
144 + self.lora_token_start_loc,
145 + self.active_lora_ids,
146 + self.no_lora_flag_cpu,
147 + )
vllm/lora/ops/triton_ops/lora_shrink.py
+12 -1
@@ -106,6 +106,7 @@ def _lora_shrink(
106 num_tokens_per_lora: torch.Tensor, # shape [max-loras + 1]
107 lora_token_start_loc: torch.Tensor, # shape [max-loras + 2]
108 lora_ids: torch.Tensor, # shape [max-loras + 1]
109 + no_lora_flag_cpu: torch.Tensor, # shape [1]
110 scaling: float,
111 ) -> None:
112 """
@@ -126,8 +127,16 @@ def _lora_shrink(
127 identifies the region in token_indices_sorted_by_lora_ids that
128 LoRA lora_ids[i] should process.
129 lora_ids (torch.Tensor): LoRA ids to process.
130 + no_lora_flag_cpu (torch.Tensor): A CPU tensor of size 1, that indicates
131 + if there are any requests that require LoRA.
132 scaling (float): Scaling factor.
133 """
134 +
135 + assert no_lora_flag_cpu.numel() == 1
136 + if no_lora_flag_cpu.item():
137 + # None of the inputs require LoRA.
138 + return
139 +
140 assert inputs.dtype == lora_a_weights[0].dtype
141 assert inputs.dtype in [torch.float16, torch.bfloat16]
142 for weight in lora_a_weights:
@@ -138,6 +147,8 @@ def _lora_shrink(
147 assert output_tensor.is_contiguous()
148
149 # metadata sanity check
150 + M = inputs.size(0)
151 + assert token_lora_mapping.size(0) == M
152 assert token_lora_mapping.size(0) == token_indices_sorted_by_lora_ids.size(
153 0)
154 assert lora_ids.size(0) == num_tokens_per_lora.size(0)
@@ -146,7 +157,6 @@ def _lora_shrink(
157 (lora_ptr_tensor, lora_strides_d0, lora_strides_d1,
158 lora_strides_d2) = _get_lora_a_ptr(lora_a_weights, inputs.device)
159 N, K = lora_a_weights[0].shape[-2:] # K=hidden_size,N=rank
149 - M = inputs.size(0)
160 NUM_SLICES = len(lora_a_weights)
161 MAX_LORAS = lora_ids.size(0)
162
@@ -218,6 +228,7 @@ def _lora_shrink_fake(
228 num_tokens_per_lora: torch.Tensor,
229 lora_token_start_loc: torch.Tensor,
230 lora_ids: torch.Tensor,
231 + no_lora_flag_cpu: torch.Tensor,
232 scaling: float,
233 ) -> None:
234 return
vllm/worker/model_runner.py
+53 -25
@@ -1242,6 +1242,29 @@ class GPUModelRunnerBase(ModelRunnerBase[TModelInputForGPU]):
1242 max_num_seqs = self.scheduler_config.max_num_seqs
1243 self._dummy_run(max_num_batched_tokens, max_num_seqs)
1244
1245 + def _add_dummy_loras(self, num_loras: int) -> list[LoRARequest]:
1246 + assert num_loras > 0
1247 + assert self.lora_manager is not None
1248 +
1249 + dummy_lora_requests: list[LoRARequest] = []
1250 + with self.lora_manager.dummy_lora_cache():
1251 + for idx in range(num_loras):
1252 + lora_id = idx + 1
1253 + dummy_lora_request = LoRARequest(
1254 + lora_name=f"warmup_{lora_id}",
1255 + lora_int_id=lora_id,
1256 + lora_path="/not/a/real/path",
1257 + )
1258 + self.lora_manager.add_dummy_lora(dummy_lora_request,
1259 + rank=LORA_WARMUP_RANK)
1260 + dummy_lora_requests.append(dummy_lora_request)
1261 + return dummy_lora_requests
1262 +
1263 + def _remove_dummy_loras(self):
1264 + # Remove dummy loras.
1265 + assert self.lora_manager is not None
1266 + self.remove_all_loras()
1267 +
1268 def _dummy_run(self,
1269 max_num_batched_tokens: int,
1270 max_num_seqs: int = 1) -> None:
@@ -1251,28 +1274,20 @@ class GPUModelRunnerBase(ModelRunnerBase[TModelInputForGPU]):
1274 SamplingParams(top_p=0.99, top_k=self.vocab_size - 1)
1275
1276 # This represents the maximum number of different requests
1254 - # that will have unique loras, an therefore the max amount of memory
1255 - # consumption create dummy lora request copies from the lora request
1256 - # passed in, which contains a lora from the lora warmup path.
1277 + # that will have unique loras, and therefore the max amount of
1278 + # memory consumption. Create dummy lora request copies from the
1279 + # lora request passed in, which contains a lora from the lora
1280 + # warmup path.
1281 dummy_lora_requests: List[LoRARequest] = []
1282 dummy_lora_requests_per_seq: List[LoRARequest] = []
1283 if self.lora_config:
1260 - assert self.lora_manager is not None
1261 - with self.lora_manager.dummy_lora_cache():
1262 - for idx in range(self.lora_config.max_loras):
1263 - lora_id = idx + 1
1264 - dummy_lora_request = LoRARequest(
1265 - lora_name=f"warmup_{lora_id}",
1266 - lora_int_id=lora_id,
1267 - lora_path="/not/a/real/path",
1268 - )
1269 - self.lora_manager.add_dummy_lora(dummy_lora_request,
1270 - rank=LORA_WARMUP_RANK)
1271 - dummy_lora_requests.append(dummy_lora_request)
1272 - dummy_lora_requests_per_seq = [
1273 - dummy_lora_requests[idx % len(dummy_lora_requests)]
1274 - for idx in range(max_num_seqs)
1275 - ]
1284 + dummy_lora_requests = self._add_dummy_loras(
1285 + self.lora_config.max_loras)
1286 + assert len(dummy_lora_requests) == self.lora_config.max_loras
1287 + dummy_lora_requests_per_seq = [
1288 + dummy_lora_requests[idx % len(dummy_lora_requests)]
1289 + for idx in range(max_num_seqs)
1290 + ]
1291
1292 # Profile memory usage with max_num_sequences sequences and the
1293 # total number of tokens equal to max_num_batched_tokens.
@@ -1354,9 +1369,8 @@ class GPUModelRunnerBase(ModelRunnerBase[TModelInputForGPU]):
1369 self.execute_model(model_input, kv_caches, intermediate_tensors)
1370 torch.cuda.synchronize()
1371 if self.lora_config:
1357 - # Remove dummy loras.
1358 - assert self.lora_manager is not None
1359 - self.remove_all_loras()
1372 + self._remove_dummy_loras()
1373 +
1374 return
1375
1376 def remove_all_loras(self):
@@ -1479,6 +1493,16 @@ class GPUModelRunnerBase(ModelRunnerBase[TModelInputForGPU]):
1493 dtype=self.model_config.dtype,
1494 device=self.device)
1495
1496 + dummy_lora_id: Optional[int] = None
1497 + dummy_lora_request: LoRARequest = []
1498 + if self.lora_config:
1499 + # The goal is to capture the LoRA kernels in cuda graphs.
1500 + # for this purpose, as single dummy lora is sufficient.
1501 + dummy_lora_requests = self._add_dummy_loras(num_loras=1)
1502 + assert len(dummy_lora_requests) == 1
1503 + dummy_lora_request = dummy_lora_requests[0]
1504 + dummy_lora_id = dummy_lora_request.lora_int_id
1505 +
1506 with self.attn_state.graph_capture(max_batch_size), graph_capture(
1507 self.device) as graph_capture_context:
1508 # NOTE: Capturing the largest batch size first may help reduce the
@@ -1503,10 +1527,11 @@ class GPUModelRunnerBase(ModelRunnerBase[TModelInputForGPU]):
1527 attn_metadata.enable_kv_scales_calculation = False
1528 if self.lora_config:
1529 lora_mapping = LoRAMapping(
1506 - **dict(index_mapping=[0] * batch_size,
1507 - prompt_mapping=[0] * batch_size,
1530 + **dict(index_mapping=[dummy_lora_id] * batch_size,
1531 + prompt_mapping=[dummy_lora_id] * batch_size,
1532 is_prefill=False))
1509 - self.set_active_loras(set(), lora_mapping)
1533 + self.set_active_loras(set([dummy_lora_request]),
1534 + lora_mapping)
1535
1536 if self.prompt_adapter_config:
1537 prompt_adapter_mapping = PromptAdapterMapping(
@@ -1562,6 +1587,9 @@ class GPUModelRunnerBase(ModelRunnerBase[TModelInputForGPU]):
1587 self.graph_runners[virtual_engine][batch_size] = (
1588 graph_runner)
1589
1590 + if self.lora_config:
1591 + self._remove_dummy_loras()
1592 +
1593 end_time = time.perf_counter()
1594 end_free_gpu_memory = torch.cuda.mem_get_info()[0]
1595 elapsed_time = end_time - start_time