-
-
Notifications
You must be signed in to change notification settings - Fork 19.1k
[LoRA] Support dual CUDA streams-Linear Layer #35721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
e1b83e7
193bbd1
ac348b2
ee4cbd8
668066b
956a551
23a2758
f09723b
1ff9932
d893daa
0f8a59d
092b206
76898e0
3dd4957
de098b0
ae6a597
90a5ede
f38ca3c
21f9c81
af9bb12
551d295
97fea1a
bd8b5a9
6abfcfd
b2b437b
9d8515b
66a734c
7077887
fd759ae
13a3d23
691e0f4
a57dedb
cfd110b
619703a
3e33425
c2dbc3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,41 +5,72 @@ | |
| import torch | ||
| from transformers import PretrainedConfig | ||
|
|
||
| from vllm.config import get_current_vllm_config | ||
| from vllm.config.lora import LoRAConfig | ||
| from vllm.distributed.utils import divide | ||
| from vllm.forward_context import ForwardContext, get_forward_context | ||
| from vllm.model_executor.layers.linear import ( | ||
| ColumnParallelLinear, | ||
| LinearBase, | ||
| ReplicatedLinear, | ||
| RowParallelLinear, | ||
| ) | ||
| from vllm.platforms import current_platform | ||
| from vllm.utils.torch_utils import current_stream, direct_register_custom_op | ||
|
|
||
| from .base import BaseLayerWithLoRA | ||
| from .utils import _get_lora_device | ||
|
|
||
| # aux_stream() is shared for: | ||
| # - LoRA dual-stream: base linear and LoRA compute on different CUDA streams | ||
| _aux_stream: torch.cuda.Stream | None = None | ||
|
|
||
|
|
||
| def aux_stream() -> torch.cuda.Stream | None: | ||
| """ | ||
| Returns the auxiliary CUDA stream for overlapping compute. | ||
| Initialized only once on CUDA-alike platforms. | ||
| """ | ||
| global _aux_stream | ||
|
|
||
| if _aux_stream is None and current_platform.is_cuda_alike(): | ||
| _aux_stream = torch.cuda.Stream() | ||
|
|
||
| return _aux_stream | ||
|
|
||
|
|
||
| class BaseLinearLayerWithLoRA(BaseLayerWithLoRA): | ||
| def __init__(self, base_layer: LinearBase): | ||
| super().__init__() | ||
|
|
||
| self._lora_stream = aux_stream() | ||
| self.base_layer = base_layer | ||
| self.input_size = self.base_layer.input_size | ||
| # Ensure tp_size and tp_rank consistency with the base_layer. | ||
| self.tp_size = self.base_layer.tp_size | ||
| self.tp_rank = self.base_layer.tp_rank | ||
| self.device = _get_lora_device(self.base_layer) | ||
| self._init_lora_stream_context() | ||
| self.output_slices: tuple[int, ...] | ||
| self.output_size: int | ||
| self.n_slices: int | ||
|
|
||
| def _init_lora_stream_context(self) -> None: | ||
| vllm_config = get_current_vllm_config() | ||
| # lora_linear avoids prefix conflicts with the base layer | ||
| self.layer_name = self.base_layer.prefix + ".lora_linear" | ||
| compilation_config = vllm_config.compilation_config | ||
| if self.layer_name in compilation_config.static_forward_context: | ||
| raise ValueError("Duplicate layer name: {}".format(self.layer_name)) | ||
| compilation_config.static_forward_context[self.layer_name] = self | ||
|
|
||
| def create_lora_weights( | ||
| self, | ||
| max_loras: int, | ||
| lora_config: LoRAConfig, | ||
| model_config: PretrainedConfig | None = None, | ||
| ) -> None: | ||
| self.lora_config = lora_config | ||
| # | ||
| if isinstance(self.base_layer, ReplicatedLinear): | ||
| lora_a_out_size = lora_config.max_lora_rank | ||
| lora_b_out_size = self.output_size | ||
|
|
@@ -120,6 +151,18 @@ def set_lora( | |
| ) | ||
|
|
||
| def apply(self, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor: | ||
| if self._lora_stream is None: | ||
| return self._apply_sync(x, bias) | ||
| else: | ||
| num_tokens = x.size(0) | ||
| output_size = sum(self.output_slices) | ||
| return torch.ops.vllm.lora_linear( | ||
| self.layer_name, num_tokens, output_size, x, bias | ||
| ) | ||
|
|
||
| def _apply_sync( | ||
| self, x: torch.Tensor, bias: torch.Tensor | None = None | ||
| ) -> torch.Tensor: | ||
| output = self.base_layer.quant_method.apply(self.base_layer, x, bias) | ||
|
|
||
| original_shape = output.shape if output.ndim == 3 else None | ||
|
|
@@ -144,6 +187,59 @@ def apply(self, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tens | |
|
|
||
| return output | ||
|
|
||
| def _apply_async_impl( | ||
| self, x: torch.Tensor, bias: torch.Tensor | None = None | ||
| ) -> torch.Tensor: | ||
| """ | ||
| Forward pass with base linear and LoRA on separate CUDA streams for overlap. | ||
| Base layer runs on default stream; LoRA runs on aux stream. | ||
| """ | ||
| assert isinstance(self._lora_stream, torch.cuda.Stream) # Make mypy happy | ||
| self._lora_stream.wait_stream(current_stream()) | ||
| output = self.base_layer.quant_method.apply(self.base_layer, x, bias) | ||
|
|
||
| # Pre-allocate buffer for LoRA-only output (add_inputs=False writes here). | ||
| # Infer shape from x and output_slices; avoid using output tensor. | ||
| num_tokens = x.size(0) if x.ndim == 2 else x.size(1) | ||
|
jeejeelee marked this conversation as resolved.
|
||
| output_size = sum(self.output_slices) | ||
|
|
||
| lora_output = torch.empty( | ||
| (num_tokens, output_size), | ||
| device=self.device, | ||
| dtype=x.dtype, | ||
| ) | ||
| with torch.cuda.stream(self._lora_stream): | ||
| # LoRA stream waits for base layer output before reading. | ||
| self._lora_stream.wait_stream(current_stream()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment on line 212 is incorrect, and the |
||
| self.punica_wrapper.add_lora_linear( | ||
| lora_output, | ||
| x, | ||
| self.lora_a_stacked, | ||
| self.lora_b_stacked, | ||
| 1.0, | ||
| self.output_slices, | ||
| add_inputs=False, | ||
| ) | ||
| # Default stream waits for LoRA to complete before combining. | ||
| current_stream().wait_stream(self._lora_stream) | ||
| original_shape = output.shape if output.ndim == 3 else None | ||
|
|
||
| # In transformers backend, x and output have extra batch dimension like | ||
| # (1, seq_len, hidden_dim), while punica expects (seq_len, hidden_dim), | ||
| # therefore we need to flatten the batch dimensions. | ||
| if x.ndim == 3 and output.ndim == 3: | ||
| output = output.flatten(0, 1) | ||
| x = x.flatten(0, 1) | ||
|
|
||
| output = output + lora_output | ||
|
|
||
| # Reshape the flattened output back to its original shape, | ||
| # as some MM encoders cannot handle flattened inputs. | ||
| if original_shape is not None: | ||
| output = output.reshape(original_shape) | ||
|
|
||
| return output | ||
|
|
||
| @property | ||
| def weight(self) -> torch.Tensor: | ||
| # unquantizedLinear | ||
|
|
@@ -167,3 +263,36 @@ def bias(self) -> torch.Tensor | None: | |
| return self.base_layer.bias | ||
| else: | ||
| return None | ||
|
|
||
|
|
||
| def lora_linear( | ||
| layer_name: str, | ||
| num_tokens: int, | ||
| output_size: int, | ||
| x: torch.Tensor, | ||
| bias: torch.Tensor | None = None, | ||
| ) -> torch.Tensor: | ||
| forward_context: ForwardContext = get_forward_context() | ||
| self = forward_context.no_compile_layers[layer_name] | ||
| return self._apply_async_impl(x, bias) | ||
|
|
||
|
|
||
| def lora_linear_fake( | ||
| layer_name: str, | ||
| num_tokens: int, | ||
| output_size: int, | ||
| x: torch.Tensor, | ||
| bias: torch.Tensor | None = None, | ||
| ) -> torch.Tensor: | ||
| return torch.empty( | ||
| (num_tokens, output_size), | ||
| device=x.device, | ||
| dtype=x.dtype, | ||
| ) | ||
|
|
||
|
|
||
| direct_register_custom_op( | ||
| op_name="lora_linear", | ||
| op_func=lora_linear, | ||
| fake_impl=lora_linear_fake, | ||
| ) | ||
Uh oh!
There was an error while loading. Please reload this page.