@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / b63f2143f8
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 14, 05:30 UTC

[LoRA] LoRA cuda graph specialization (#25914)

Signed-off-by: Andy Lo <andy@mistral.ai> Co-authored-by: Jee Jee Li <pandaleefree@gmail.com>

Andy Lo committed Oct 20, 2025 at 05:21 UTC b63f2143f8db6ef18c34eb25237fd06c5ad1c450
9 files changed +122 -34
tests/lora/test_chatglm3_tp.py
+15 -6
@@ -2,6 +2,7 @@
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import vllm
5 +import vllm.config
6 from vllm.lora.request import LoRARequest
7
8 from ..utils import create_new_process_for_each_test, multi_gpu_test
@@ -53,9 +54,10 @@ def do_sample(llm: vllm.LLM, lora_path: str, lora_id: int) -> list[str]:
54 def test_chatglm3_lora(chatglm3_lora_files):
55 llm = vllm.LLM(
56 MODEL_PATH,
56 - max_model_len=1024,
57 + max_model_len=512,
58 enable_lora=True,
58 - max_loras=4,
59 + max_loras=2,
60 + max_num_seqs=16,
61 max_lora_rank=64,
62 trust_remote_code=True,
63 )
@@ -72,13 +74,17 @@ def test_chatglm3_lora(chatglm3_lora_files):
74 def test_chatglm3_lora_tp4(chatglm3_lora_files):
75 llm = vllm.LLM(
76 MODEL_PATH,
75 - max_model_len=1024,
77 + max_model_len=512,
78 enable_lora=True,
77 - max_loras=4,
79 + max_loras=2,
80 max_lora_rank=64,
81 + max_num_seqs=16,
82 tensor_parallel_size=4,
83 trust_remote_code=True,
84 fully_sharded_loras=False,
85 + compilation_config=vllm.config.CompilationConfig( # Avoid OOM
86 + cudagraph_specialize_lora=False,
87 + ),
88 )
89
90 output1 = do_sample(llm, chatglm3_lora_files, lora_id=1)
@@ -96,14 +102,17 @@ def test_chatglm3_lora_tp4_fully_sharded_loras(chatglm3_lora_files):
102 # more GPU memory causing vLLM to OOM
103 llm = vllm.LLM(
104 MODEL_PATH,
99 - max_model_len=1024,
105 + max_model_len=512,
106 enable_lora=True,
101 - max_loras=4,
107 + max_loras=2,
108 max_lora_rank=64,
109 tensor_parallel_size=4,
110 trust_remote_code=True,
111 fully_sharded_loras=True,
112 gpu_memory_utilization=0.85,
113 + compilation_config=vllm.config.CompilationConfig( # Avoid OOM
114 + cudagraph_specialize_lora=False,
115 + ),
116 )
117 output1 = do_sample(llm, chatglm3_lora_files, lora_id=1)
118 for i in range(len(EXPECTED_LORA_OUTPUT)):
tests/lora/test_llama_tp.py
+8 -1
@@ -3,7 +3,10 @@
3 import subprocess
4 import sys
5
6 +import pytest
7 +
8 import vllm
9 +import vllm.config
10 from vllm import LLM
11 from vllm.lora.request import LoRARequest
12 from vllm.model_executor.model_loader.tensorizer import TensorizerConfig
@@ -100,7 +103,8 @@ def generate_and_test(llm, sql_lora_files, tensorizer_config_dict: dict | None =
103
104
105 @create_new_process_for_each_test()
103 -def test_llama_lora(sql_lora_files):
106 +@pytest.mark.parametrize("cudagraph_specialize_lora", [True, False])
107 +def test_llama_lora(sql_lora_files, cudagraph_specialize_lora: bool):
108 llm = vllm.LLM(
109 MODEL_PATH,
110 tokenizer=sql_lora_files,
@@ -108,6 +112,9 @@ def test_llama_lora(sql_lora_files):
112 # also test odd max_num_seqs
113 max_num_seqs=13,
114 max_loras=4,
115 + compilation_config=vllm.config.CompilationConfig(
116 + cudagraph_specialize_lora=cudagraph_specialize_lora,
117 + ),
118 )
119 generate_and_test(llm, sql_lora_files)
120
vllm/config/compilation.py
+8
@@ -366,6 +366,14 @@ class CompilationConfig:
366 minor release, i.e. v0.11.0 or v1.0.0. Please use cudagraph_mode=
367 FULL_AND_PIECEWISE instead.
368 """
369 + cudagraph_specialize_lora: bool = True
370 + """Whether to create separate cuda graphs for cases with and without active
371 + LoRA adapters. When set to False, the LoRA-enabled cuda graph will be used
372 + for all cases, incurring the overhead of running LoRA ops even when no
373 + adapters are active. Setting this to True will remove this overhead at the
374 + cost of increased startup time and slightly higher memory usage.
375 + When `enable_lora` is False, this option has no effect.
376 + """
377
378 use_inductor_graph_partition: bool = False
379 """Use inductor graph partition to split the graph at cudagraph_unsafe ops.
vllm/forward_context.py
+7 -1
@@ -40,13 +40,19 @@ class BatchDescriptor(NamedTuple):
40 False can also be used for an uniform decode batch to dispatch to the
41 cudagraph supporting non-uniform batches.
42 """
43 + has_lora: bool = False
44 + """
45 + Whether this batch has active LoRA adapters.
46 + """
47
48 @property
49 def non_uniform(self) -> "BatchDescriptor":
50 """
51 Return a non-uniform version of current batch descriptor.
52 """
49 - return BatchDescriptor(self.num_tokens, uniform_decode=False)
53 + return BatchDescriptor(
54 + self.num_tokens, uniform_decode=False, has_lora=self.has_lora
55 + )
56
57
58 def _compute_sp_num_tokens(
vllm/lora/ops/triton_ops/lora_shrink_op.py
+2
@@ -169,6 +169,8 @@ def _lora_shrink(
169 assert lora_ids.size(0) == num_tokens_per_lora.size(0)
170 assert lora_token_start_loc.size(0) == lora_ids.size(0) + 1
171
172 + output_tensor.zero_()
173 +
174 (lora_ptr_tensor, lora_strides_d0, lora_strides_d1, lora_strides_d2) = (
175 _get_lora_a_ptr(lora_a_weights, inputs.device)
176 )
vllm/lora/punica_wrapper/punica_gpu.py
+21 -13
@@ -205,15 +205,18 @@ class PunicaWrapperGPU(PunicaWrapperBase):
205
206 assert len(lora_a_stacked) == len(lora_b_stacked) == len(output_slices)
207
208 - if buffer is None:
209 - r = lora_b_stacked[0].size(-1)
210 - # We set the buffer to be float32 by default, refer to:
211 - # https://github.com/triton-lang/triton/issues/1387
212 - buffer = torch.zeros( # type: ignore
213 - (len(output_slices), x.size(0), r),
214 - dtype=torch.float32,
215 - device=x.device,
216 - )
208 + assert buffer is None, (
209 + "To minimize overhead, the buffer should be created by "
210 + ".add_lora_linear() instead of being passed in."
211 + )
212 + r = lora_b_stacked[0].size(-1)
213 + # We set the buffer to be float32 by default, refer to:
214 + # https://github.com/triton-lang/triton/issues/1387
215 + # Note: buffer is zeroed inside the shrink op
216 + buffer = torch.empty(
217 + (len(output_slices), x.size(0), r), dtype=torch.float32, device=x.device
218 + )
219 +
220 self.add_shrink(
221 buffer, # type: ignore
222 x,
@@ -260,10 +263,15 @@ class PunicaWrapperGPU(PunicaWrapperBase):
263 y = y.view(-1, y.shape[-1])
264 x = x.view(-1, x.shape[-1])
265 r = lora_b_stacked.size(-1)
263 - if buffer is None:
264 - # We set the buffer to be float32 by default, refer to:
265 - # https://github.com/triton-lang/triton/issues/1387
266 - buffer = torch.zeros((x.size(0), r), dtype=torch.float32, device=x.device)
266 +
267 + assert buffer is None, (
268 + "To minimize overhead, the buffer should be created by "
269 + ".add_lora_linear() instead of being passed in."
270 + )
271 + # We set the buffer to be float32 by default, refer to:
272 + # https://github.com/triton-lang/triton/issues/1387
273 + # Note: buffer is zeroed inside the shrink op
274 + buffer = torch.empty((x.size(0), r), dtype=torch.float32, device=x.device)
275
276 lora_shrink(
277 x,
vllm/v1/cudagraph_dispatcher.py
+20 -4
@@ -1,5 +1,6 @@
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +from itertools import product
4
5 from vllm.config import CUDAGraphMode, VllmConfig
6 from vllm.forward_context import BatchDescriptor
@@ -67,14 +68,27 @@ class CudagraphDispatcher:
68 ):
69 # This should be called only after attention backend is initialized.
70
71 + # LoRA activation cases to specialize the cuda graphs on
72 + if self.vllm_config.lora_config:
73 + if self.compilation_config.cudagraph_specialize_lora:
74 + lora_cases = [True, False]
75 + else:
76 + lora_cases = [True]
77 + else:
78 + lora_cases = [False]
79 +
80 # Note: we create all valid keys for cudagraph here but do not
81 # guarantee all keys would be used. For example, if we allow lazy
82 # capturing in future PR, some keys may never be triggered.
83 if cudagraph_mode.mixed_mode() != CUDAGraphMode.NONE:
74 - for bs in self.compilation_config.cudagraph_capture_sizes:
84 + for bs, has_lora in product(
85 + self.compilation_config.cudagraph_capture_sizes, lora_cases
86 + ):
87 self.add_cudagraph_key(
88 cudagraph_mode.mixed_mode(),
77 - BatchDescriptor(num_tokens=bs, uniform_decode=False),
89 + BatchDescriptor(
90 + num_tokens=bs, uniform_decode=False, has_lora=has_lora
91 + ),
92 )
93
94 # if decode cudagraph mode is FULL, and we don't already have mixed
@@ -92,10 +106,12 @@ class CudagraphDispatcher:
106 for x in self.compilation_config.cudagraph_capture_sizes
107 if x <= max_num_tokens and x >= uniform_decode_query_len
108 ]
95 - for bs in cudagraph_capture_sizes_for_decode:
109 + for bs, has_lora in product(cudagraph_capture_sizes_for_decode, lora_cases):
110 self.add_cudagraph_key(
111 CUDAGraphMode.FULL,
98 - BatchDescriptor(num_tokens=bs, uniform_decode=True),
112 + BatchDescriptor(
113 + num_tokens=bs, uniform_decode=True, has_lora=has_lora
114 + ),
115 )
116 self.keys_initialized = True
117
vllm/v1/worker/gpu_model_runner.py
+27 -6
@@ -8,6 +8,7 @@ from collections import defaultdict
8 from collections.abc import Iterator
9 from contextlib import contextmanager
10 from copy import deepcopy
11 +from itertools import product
12 from typing import TYPE_CHECKING, Any, NamedTuple, TypeAlias, cast
13
14 import numpy as np
@@ -2469,7 +2470,9 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
2470 num_scheduled_tokens == self.input_batch.num_reqs * max_query_len
2471 )
2472 batch_descriptor = BatchDescriptor(
2472 - num_tokens=num_input_tokens, uniform_decode=uniform_decode
2473 + num_tokens=num_input_tokens,
2474 + uniform_decode=uniform_decode,
2475 + has_lora=len(self.input_batch.lora_id_to_lora_request) > 0,
2476 )
2477 cudagraph_runtime_mode, batch_descriptor = (
2478 self.cudagraph_dispatcher.dispatch(batch_descriptor, use_cascade_attn)
@@ -3193,6 +3196,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3196 is_profile: bool = False,
3197 create_mixed_batch: bool = False,
3198 remove_lora: bool = True,
3199 + activate_lora: bool = False,
3200 ) -> tuple[torch.Tensor, torch.Tensor]:
3201 """
3202 Run a dummy forward pass to warm up/profile run or capture the
@@ -3215,6 +3219,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3219 create_mixed_batch: If True, create a mixed batch with both decode
3220 (1 token) and prefill (multiple tokens) requests.
3221 remove_lora: If False, dummy LoRAs are not destroyed after the run
3222 + activate_lora: If False, dummy_run is performed without LoRAs.
3223 """
3224 assert (
3225 cudagraph_runtime_mode is None
@@ -3364,7 +3369,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3369 attn_metadata[layer_name] = attn_metadata_i
3370
3371 with self.maybe_dummy_run_with_lora(
3367 - self.lora_config, num_scheduled_tokens, remove_lora
3372 + self.lora_config, num_scheduled_tokens, activate_lora, remove_lora
3373 ):
3374 # Make sure padding doesn't exceed max_num_tokens
3375 assert num_tokens_after_padding <= self.max_num_tokens
@@ -3411,6 +3416,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3416 BatchDescriptor(
3417 num_tokens=num_tokens_after_padding,
3418 uniform_decode=uniform_decode,
3419 + has_lora=activate_lora and self.lora_config is not None,
3420 )
3421 )
3422 if not is_profile
@@ -3769,10 +3775,21 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3775 start_free_gpu_memory = torch.cuda.mem_get_info()[0]
3776 cudagraph_mode = self.compilation_config.cudagraph_mode
3777 assert cudagraph_mode is not None
3778 +
3779 + if self.lora_config:
3780 + if self.compilation_config.cudagraph_specialize_lora:
3781 + lora_cases = [True, False]
3782 + else:
3783 + lora_cases = [True]
3784 + else:
3785 + lora_cases = [False]
3786 +
3787 if cudagraph_mode.mixed_mode() != CUDAGraphMode.NONE:
3788 cudagraph_runtime_mode = cudagraph_mode.mixed_mode()
3789
3775 - compilation_cases = list(reversed(self.cudagraph_batch_sizes))
3790 + compilation_cases = list(
3791 + product(reversed(self.cudagraph_batch_sizes), lora_cases)
3792 + )
3793 self._capture_cudagraphs(
3794 compilation_cases,
3795 cudagraph_runtime_mode=cudagraph_runtime_mode,
@@ -3793,7 +3810,9 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3810 for x in self.cudagraph_batch_sizes
3811 if max_num_tokens >= x >= self.uniform_decode_query_len
3812 ]
3796 - compilation_cases_decode = list(reversed(decode_cudagraph_batch_sizes))
3813 + compilation_cases_decode = list(
3814 + product(reversed(decode_cudagraph_batch_sizes), lora_cases)
3815 + )
3816 self._capture_cudagraphs(
3817 compilation_cases=compilation_cases_decode,
3818 cudagraph_runtime_mode=CUDAGraphMode.FULL,
@@ -3823,7 +3842,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3842
3843 def _capture_cudagraphs(
3844 self,
3826 - compilation_cases: list[int],
3845 + compilation_cases: list[tuple[int, bool]],
3846 cudagraph_runtime_mode: CUDAGraphMode,
3847 uniform_decode: bool,
3848 ):
@@ -3844,7 +3863,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3863 )
3864
3865 # We skip EPLB here since we don't want to record dummy metrics
3847 - for num_tokens in compilation_cases:
3866 + for num_tokens, activate_lora in compilation_cases:
3867 # We currently only capture ubatched graphs when its a FULL
3868 # cudagraph, a uniform decode batch, and the number of tokens
3869 # is above the threshold. Otherwise we just capture a non-ubatched
@@ -3875,6 +3894,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3894 allow_microbatching=allow_microbatching,
3895 skip_eplb=True,
3896 remove_lora=False,
3897 + activate_lora=activate_lora,
3898 )
3899 self._dummy_run(
3900 num_tokens,
@@ -3883,6 +3903,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
3903 allow_microbatching=allow_microbatching,
3904 skip_eplb=True,
3905 remove_lora=False,
3906 + activate_lora=activate_lora,
3907 )
3908 self.maybe_remove_all_loras(self.lora_config)
3909
vllm/v1/worker/lora_model_runner_mixin.py
+14 -3
@@ -120,7 +120,10 @@ class LoRAModelRunnerMixin:
120
121 @contextmanager
122 def maybe_select_dummy_loras(
123 - self, lora_config: LoRAConfig | None, num_scheduled_tokens: np.ndarray
123 + self,
124 + lora_config: LoRAConfig | None,
125 + num_scheduled_tokens: np.ndarray,
126 + activate_lora: bool = True,
127 ):
128 if lora_config is None:
129 yield
@@ -133,7 +136,12 @@ class LoRAModelRunnerMixin:
136
137 # Make prompt lora mapping
138 # Assign LoRA IDs cyclically to simulate a worst-case scenario.
136 - prompt_lora_mapping = (np.arange(num_reqs, dtype=np.int32) % num_loras) + 1
139 + if activate_lora:
140 + prompt_lora_mapping = (
141 + np.arange(num_reqs, dtype=np.int32) % num_loras
142 + ) + 1
143 + else:
144 + prompt_lora_mapping = np.zeros(num_reqs, dtype=np.int32)
145
146 # Make token lora mapping
147 token_lora_mapping = np.repeat(prompt_lora_mapping, num_scheduled_tokens)
@@ -159,11 +167,14 @@ class LoRAModelRunnerMixin:
167 self,
168 lora_config: LoRAConfig | None,
169 num_scheduled_tokens: np.ndarray,
170 + activate_lora: bool = True,
171 remove_lora: bool = True,
172 ):
173 with (
174 self.maybe_setup_dummy_loras(lora_config, remove_lora),
166 - self.maybe_select_dummy_loras(lora_config, num_scheduled_tokens),
175 + self.maybe_select_dummy_loras(
176 + lora_config, num_scheduled_tokens, activate_lora
177 + ),
178 ):
179 yield
180