main
py 757 lines 27.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5 import torch
6 import torch.nn as nn
7 from transformers import PreTrainedConfig
8
9 from vllm.config.lora import LoRAConfig
10 from vllm.distributed import tensor_model_parallel_all_gather
11 from vllm.distributed.utils import divide
12 from vllm.model_executor.custom_op import maybe_get_oot_by_class
13 from vllm.model_executor.layers.linear import (
14 ColumnParallelLinear,
15 MergedColumnParallelLinear,
16 QKVParallelLinear,
17 )
18 from vllm.platforms import current_platform
19
20 from .base_linear import BaseLinearLayerWithLoRA
21 from .utils import _fully_sharded_can_replace, _not_fully_sharded_can_replace
22
23
24 def _mcp_apply(x, bias, layer: "ColumnParallelLinearWithLoRA"):
25 """Fully-sharded (S-LoRA) apply path for column-parallel LoRA layers."""
26 assert layer.lora_config.fully_sharded_loras, (
27 "_mcp_apply is only used for fully sharded LoRA"
28 )
29 assert (
30 layer.n_slices
31 == len(layer.lora_a_stacked)
32 == len(layer.lora_b_stacked)
33 == len(layer.output_slices)
34 )
35
36 output = layer._get_quant_method().apply(layer.base_layer, x, bias)
37
38 x = x.view(-1, x.shape[-1])
39 output, out_orig_shape = output.view(-1, output.shape[-1]), output.shape
40
41 # Since communication is needed, the buffer is directly initialized as a
42 # tensor rather than a tuple of tensor.
43 local_lora_rank = layer.lora_a_stacked[0].shape[2]
44 buffer_shape = (layer.n_slices, x.shape[0], local_lora_rank)
45 # Under torch.compile, the local-rank-1 fully-sharded path can otherwise
46 # get lowered to a reinterpret view with a non-canonical layout. The
47 # Triton shrink op mutates this buffer in place and expects the standard
48 # contiguous [slice, token, rank] stride contract.
49 buffers = torch.empty_strided(
50 buffer_shape,
51 (x.shape[0] * local_lora_rank, local_lora_rank, 1),
52 dtype=torch.float32,
53 device=x.device,
54 )
55 buffers.zero_()
56
57 shrunk_buffers: torch.Tensor | None = layer.punica_wrapper.add_shrink(
58 buffers, x, layer.lora_a_stacked, 1.0
59 )
60
61 if not current_platform.can_update_inplace():
62 buffers = shrunk_buffers
63
64 buffers = tensor_model_parallel_all_gather(buffers)
65
66 lora_output: torch.Tensor | None = layer.punica_wrapper.add_expand(
67 output,
68 buffers,
69 layer.lora_b_stacked,
70 layer.output_slices,
71 offset_start=0,
72 add_input=True,
73 )
74
75 if not current_platform.can_update_inplace():
76 if lora_output is None:
77 raise RuntimeError("LoRA expand must return an output tensor.")
78 output = lora_output
79
80 output = output.view(*out_orig_shape)
81 # now have column partitioned and packed output
82 return output
83
84
85 class ColumnParallelLinearWithLoRA(BaseLinearLayerWithLoRA):
86 """LoRA on top of ColumnParallelLinear layer.
87 LoRA B is sliced for tensor parallelism.
88 There are two types for the `base_layer`:
89 1. ColumnParallelLinear, e.g.`dense_h_to_4h` in `FalconForCausalLM`.
90 2. MergedColumnParallelLinear, e.g.`gate_up_proj` in `Phi3ForCausalLM`.
91 """
92
93 def __init__(self, base_layer: ColumnParallelLinear) -> None:
94 super().__init__(base_layer)
95 # The base_layer type is ColumnParallelLinear or
96 # MergedColumnParallelLinear, their weight sharding logic is
97 # inconsistent when TP is greater than 1.
98 self.is_merged_col_linear = isinstance(base_layer, MergedColumnParallelLinear)
99 self.output_size = self.base_layer.output_size_per_partition
100 # There is only one LoRA layer
101 self.n_slices = 1
102
103 def slice_lora_a(self, lora_a: torch.Tensor) -> torch.Tensor:
104 return lora_a
105
106 def slice_lora_b(self, lora_b: torch.Tensor) -> torch.Tensor:
107 # Applicable to cases where the base_layer is
108 # MergedColumnParallelLinear.
109 if self.is_merged_col_linear:
110 shard_size = self.output_size // 2
111 offset = lora_b.shape[0] // 2
112
113 left_weight = lora_b[
114 self.tp_rank * shard_size : (self.tp_rank + 1) * shard_size, :
115 ]
116 right_weight = lora_b[
117 offset + self.tp_rank * shard_size : offset
118 + (self.tp_rank + 1) * shard_size,
119 :,
120 ]
121 lora_b = torch.cat([left_weight, right_weight], dim=0)
122 # Applicable to cases where the base_layer is
123 # ColumnParallelLinear.
124 else:
125 shard_size = self.output_size
126 start_idx = self.tp_rank * shard_size
127 end_idx = (self.tp_rank + 1) * shard_size
128 lora_b = lora_b[start_idx:end_idx, :]
129 return lora_b
130
131 def forward(
132 self, input_: torch.Tensor
133 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor | None]:
134 """Forward of ColumnParallelLinear
135
136 Args:
137 input_: Tensor whose last dimension is `input_size`.
138
139 Returns:
140 - output
141 - bias
142
143 """
144 bias = self.base_layer.bias if not self.base_layer.skip_bias_add else None
145
146 # Matrix multiply.
147 output_parallel = self.apply(input_, bias)
148 if self.base_layer.gather_output and self.tp_size > 1:
149 # All-gather across the partitions.
150 output = tensor_model_parallel_all_gather(output_parallel)
151 else:
152 output = output_parallel
153
154 if not self.base_layer.return_bias:
155 return output
156
157 output_bias = self.base_layer.bias if self.base_layer.skip_bias_add else None
158 return output, output_bias
159
160 @classmethod
161 @_not_fully_sharded_can_replace
162 def can_replace_layer(
163 cls,
164 source_layer: nn.Module,
165 lora_config: LoRAConfig,
166 packed_modules_list: list,
167 model_config: PreTrainedConfig | None = None,
168 ) -> bool:
169 if type(source_layer) is maybe_get_oot_by_class(ColumnParallelLinear):
170 return True
171 if isinstance(source_layer, maybe_get_oot_by_class(MergedColumnParallelLinear)):
172 if len(packed_modules_list) != 1:
173 return False
174 # Exclude layers with 3+ output sizes - those are handled by
175 # MergedColumnParallelLinearVariableSliceWithLoRA since this
176 # class's slice_lora_b assumes exactly 2 slices.
177 return not (
178 hasattr(source_layer, "output_sizes")
179 and len(source_layer.output_sizes) >= 3
180 )
181 return False
182
183
184 class MergedColumnParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
185 """ColumnParallelLinear layer that is composed of 2 sublayers (slices)
186 packed together (e.g. gate_proj + up_proj -> gate_up_proj).
187
188 This means we have 2 LoRAs, each applied to one half of the layer.
189
190 Both slices must have the same size.
191 """
192
193 def __init__(
194 self, base_layer: MergedColumnParallelLinear | QKVParallelLinear
195 ) -> None:
196 super().__init__(base_layer)
197 # There are two LoRA layers
198 # the output_sizes in MergedColumnParallelLinear is not sharded by tp
199 # we need to divide it by the tp_size to get correct slices size
200 self.output_sizes = self.base_layer.output_sizes
201 self.output_slices = tuple(
202 divide(output_size, self.tp_size) for output_size in self.output_sizes
203 )
204 self.n_slices = len(self.output_slices)
205 self.output_ids = (self.tp_rank,) * self.n_slices
206
207 def create_lora_weights(
208 self,
209 max_loras: int,
210 lora_config: LoRAConfig,
211 model_config: PreTrainedConfig | None = None,
212 ) -> None:
213 """The main reason for overriding this function is to enhance code
214 maintainability.
215 """
216 self.lora_config = lora_config
217
218 lora_a_output_size_per_partition = (
219 lora_config.max_lora_rank
220 if not lora_config.fully_sharded_loras
221 else divide(lora_config.max_lora_rank, self.tp_size)
222 )
223
224 self.lora_a_stacked = tuple(
225 torch.zeros(
226 max_loras,
227 1,
228 lora_a_output_size_per_partition,
229 self.input_size,
230 dtype=lora_config.lora_dtype,
231 device=self.device,
232 )
233 for _ in range(self.n_slices)
234 )
235 self.lora_b_stacked = tuple(
236 torch.zeros(
237 max_loras,
238 1,
239 output_size,
240 lora_config.max_lora_rank,
241 dtype=lora_config.lora_dtype,
242 device=self.device,
243 )
244 for output_size in self.output_slices
245 )
246
247 def slice_lora_a(
248 self, lora_a: list[torch.Tensor | None]
249 ) -> list[torch.Tensor | None]:
250 return lora_a
251
252 def slice_lora_b(
253 self, lora_b: list[torch.Tensor | None]
254 ) -> list[torch.Tensor | None]:
255 sliced_lora_b = [None] * self.n_slices
256 for i, (shard_id, shard_size) in enumerate(
257 zip(self.output_ids, self.output_slices)
258 ):
259 if (lora_b_i := lora_b[i]) is not None:
260 sliced_lora_b[i] = lora_b_i[
261 shard_size * shard_id : shard_size * (shard_id + 1), :
262 ]
263 return sliced_lora_b
264
265 def expand_packed_lora(
266 self,
267 lora_a: list[torch.Tensor | None],
268 lora_b: list[torch.Tensor | None],
269 ) -> tuple[list[torch.Tensor | None], list[torch.Tensor | None]]:
270 """Expand packed adapter groups when they don't match n_slices.
271 E.g. in_proj_qkv (covers Q+K+V) + in_proj_z.
272
273 A None group member means that member was not adapted; the slice(s)
274 it covers are emitted as None placeholders so subsequent groups stay
275 aligned and those slices are left at base weights. This matches the
276 None-tolerance already present in slice_lora_b() and the set_lora()
277 stacking loop.
278 """
279 expanded_a: list[torch.Tensor | None] = []
280 expanded_b: list[torch.Tensor | None] = []
281 start_idx = 0
282 for a_i, b_i in zip(lora_a, lora_b):
283 if b_i is None:
284 # Unadapted group member: its row count is unknown (the tensor
285 # is missing), so infer its coverage as the remaining slices.
286 # This is exact for the only layout that reaches this path,
287 # the fused GDN in_proj_qkvz group, whose sole multi-slice
288 # member (in_proj_qkv, Q+K+V) leads and whose only optional
289 # member (in_proj_z) trails.
290 covered = self.n_slices - start_idx
291 for _ in range(covered):
292 expanded_a.append(None)
293 expanded_b.append(None)
294 start_idx += covered
295 continue
296 # Determine which output slices this b_i covers.
297 b_rows, cu_rows, covered = b_i.shape[0], 0, 0
298 for i in range(start_idx, self.n_slices):
299 cu_rows += self.output_sizes[i]
300 if cu_rows == b_rows:
301 covered = i - start_idx + 1
302 break
303 else:
304 raise ValueError(
305 f"Cannot determine how to split lora_b with {b_rows} rows "
306 f"into {self.n_slices} slices with output sizes "
307 f"{self.output_sizes} starting from index {start_idx}."
308 )
309 # Split b_i into per-slice tensors and replicate a_i for each.
310 start = 0
311 for j in range(covered):
312 size = self.output_sizes[start_idx + j]
313 expanded_b.append(b_i[start : start + size, :])
314 expanded_a.append(a_i)
315 start += size
316 start_idx += covered
317 return expanded_a, expanded_b
318
319 def set_lora(
320 self,
321 index: int,
322 lora_a: torch.Tensor | list[torch.Tensor],
323 lora_b: torch.Tensor | list[torch.Tensor],
324 ):
325 self.reset_lora(index)
326
327 # Expand packed adapter groups when they don't match n_slices.
328 # E.g. in_proj_qkv (covers Q+K+V) + in_proj_z as 2 groups for a
329 # 4-slice layer: split b_qkv by output_sizes and replicate a_qkv.
330 if isinstance(lora_b, list) and len(lora_b) != self.n_slices:
331 lora_a, lora_b = self.expand_packed_lora(lora_a, lora_b)
332
333 if self.tp_size > 1:
334 lora_a = self.slice_lora_a(lora_a)
335 lora_b = self.slice_lora_b(lora_b)
336
337 for i in range(self.n_slices):
338 if (lora_a_i := lora_a[i]) is not None:
339 self.lora_a_stacked[i][
340 index, 0, : lora_a_i.shape[0], : lora_a_i.shape[1]
341 ].copy_(lora_a_i, non_blocking=True)
342 if (lora_b_i := lora_b[i]) is not None:
343 self.lora_b_stacked[i][
344 index, 0, : lora_b_i.shape[0], : lora_b_i.shape[1]
345 ].copy_(lora_b_i, non_blocking=True)
346
347 def apply(self, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor:
348 merged_cls = maybe_get_oot_by_class(MergedColumnParallelLinear)
349 base_forward = getattr(type(self.base_layer), "forward", None)
350 merged_forward = getattr(merged_cls, "forward", None)
351 # Effectively unsharded subclasses can safely reuse their custom
352 # forward() implementation before applying the LoRA delta.
353 if (
354 self.tp_size == 1
355 and type(self.base_layer) is not merged_cls
356 and base_forward is not None
357 and merged_forward is not None
358 and base_forward is not merged_forward
359 ):
360 return self._apply_base_forward(x)
361 return super().apply(x, bias)
362
363 @classmethod
364 def can_replace_layer(
365 cls,
366 source_layer: nn.Module,
367 lora_config: LoRAConfig,
368 packed_modules_list: list,
369 model_config: PreTrainedConfig | None = None,
370 decorate: bool = True,
371 ) -> bool:
372 merged_cls = maybe_get_oot_by_class(MergedColumnParallelLinear)
373 if not isinstance(source_layer, merged_cls) or len(packed_modules_list) != 2:
374 return False
375
376 tp_size = getattr(source_layer, "tp_size", 1)
377 if type(source_layer) is merged_cls:
378 if not decorate:
379 return True
380 return not lora_config.fully_sharded_loras or tp_size == 1
381
382 # Only support effectively unsharded subclasses here. Sharded
383 # subclasses may have custom communication semantics that the generic
384 # merged-column LoRA path does not know how to preserve.
385 return tp_size == 1
386
387
388 class QKVParallelLinearWithLoRA(ColumnParallelLinearWithLoRA):
389 """ColumnParallelLinear layer that is specifically designed for
390 qkv_proj. Certain models, such as chatglm3 and baichuan-7b,
391 only contains a single LoRA within their qkv_proj layer.
392
393 During inference with Tensor Parallel, the weights of lora_b
394 must be accurately partitioned according to the respective ranks.
395
396 Q slice may have different shape than K and V slices (which both have
397 the same shape).
398 """
399
400 def __init__(self, base_layer: QKVParallelLinear) -> None:
401 super().__init__(base_layer)
402 self.q_proj_total_size = (
403 self.base_layer.total_num_heads * self.base_layer.head_size
404 )
405 self.q_proj_shard_size = self.base_layer.num_heads * self.base_layer.head_size
406 self.kv_proj_shard_size = (
407 self.base_layer.num_kv_heads * self.base_layer.head_size
408 )
409 self.kv_proj_total_size = (
410 self.base_layer.total_num_kv_heads * self.base_layer.head_size
411 )
412 # There is only one LoRA layer
413 self.n_slices = 1
414
415 def slice_lora_b(self, lora_b: torch.Tensor) -> torch.Tensor:
416 self.q_shard_id = self.tp_rank
417 self.kv_shard_id = self.tp_rank // self.base_layer.num_kv_head_replicas
418 lora_b_q = lora_b[
419 self.q_proj_shard_size * self.q_shard_id : self.q_proj_shard_size
420 * (self.q_shard_id + 1),
421 :,
422 ]
423 k_offset = self.q_proj_total_size
424 lora_b_k = lora_b[
425 k_offset + self.kv_proj_shard_size * self.kv_shard_id : k_offset
426 + self.kv_proj_shard_size * (self.kv_shard_id + 1),
427 :,
428 ]
429 v_offset = k_offset + self.kv_proj_total_size
430 lora_b_v = lora_b[
431 v_offset + self.kv_proj_shard_size * self.kv_shard_id : v_offset
432 + self.kv_proj_shard_size * (self.kv_shard_id + 1),
433 :,
434 ]
435 lora_b = torch.cat([lora_b_q, lora_b_k, lora_b_v], dim=0)
436 return lora_b
437
438 @classmethod
439 @_not_fully_sharded_can_replace
440 def can_replace_layer(
441 cls,
442 source_layer: nn.Module,
443 lora_config: LoRAConfig,
444 packed_modules_list: list,
445 model_config: PreTrainedConfig | None = None,
446 ) -> bool:
447 return (
448 type(source_layer) is maybe_get_oot_by_class(QKVParallelLinear)
449 and len(packed_modules_list) == 1
450 )
451
452
453 class MergedQKVParallelLinearWithLoRA(MergedColumnParallelLinearWithLoRA):
454 """MergedColumnParallelLinear layer that is composed of 3 sublayers (slices)
455 packed together in qkv proj fashion
456 (q_proj + k_proj + v_proj -> qkv_proj).
457
458 This means we have 3 LoRAs, each applied to one slice of the layer.
459
460 Q slice may have different shape than K and V slices (which both have
461 the same shape).
462 """
463
464 def __init__(self, base_layer: QKVParallelLinear) -> None:
465 super().__init__(base_layer)
466 # There are three LoRA layer.
467 self.n_slices = len(self.base_layer.output_sizes)
468
469 self.q_proj_shard_size = self.base_layer.num_heads * self.base_layer.head_size
470 self.kv_proj_shard_size = (
471 self.base_layer.num_kv_heads * self.base_layer.head_size
472 )
473 self.q_shard_id = self.tp_rank
474 self.kv_shard_id = self.tp_rank // self.base_layer.num_kv_head_replicas
475
476 self.output_slices = (
477 self.q_proj_shard_size,
478 self.kv_proj_shard_size,
479 self.kv_proj_shard_size,
480 )
481 self.output_ids = (
482 self.q_shard_id,
483 self.kv_shard_id,
484 self.kv_shard_id,
485 )
486
487 def create_lora_weights(
488 self,
489 max_loras: int,
490 lora_config: LoRAConfig,
491 model_config: PreTrainedConfig | None = None,
492 ) -> None:
493 """The main reason for overloading this function is to handle inconsistent
494 weight dimensions in qkv lora.
495 """
496 super().create_lora_weights(max_loras, lora_config, model_config)
497
498 @classmethod
499 @_not_fully_sharded_can_replace
500 def can_replace_layer(
501 cls,
502 source_layer: nn.Module,
503 lora_config: LoRAConfig,
504 packed_modules_list: list,
505 model_config: PreTrainedConfig | None = None,
506 decorate: bool = True,
507 ) -> bool:
508 return (
509 type(source_layer) is maybe_get_oot_by_class(QKVParallelLinear)
510 and len(packed_modules_list) == 3
511 )
512
513
514 # These following layers are based on the tensor parallelism strategy given in
515 # Y. Sheng et al., S-LoRA: Serving Thousands of Concurrent LoRA Adapters. 2023,
516 # https://arxiv.org/abs/2311.03285.
517
518
519 class ColumnParallelLinearWithShardedLoRA(ColumnParallelLinearWithLoRA):
520 """Differs from ColumnParallelLinearWithLoRA by slicing LoRA A also.
521
522 Based on S-LoRA, slicing happens along the rank dim.
523 """
524
525 # For all LoRA layers where the `base_layer` is `ColumnParallelLinear`,
526 # their `lora_a` and `lora_b` have different sharding patterns. After
527 # completing the `lora_a` GEMM , a gather operation is performed.
528 # Therefore, the sharding of `lora_a` only needs to correspond with the
529 # gather operation.
530 def slice_lora_a(self, lora_a: torch.Tensor) -> torch.Tensor:
531 shard_size = self.lora_a_stacked[0].shape[2]
532 start_idx = self.tp_rank * shard_size
533 lora_a = lora_a[start_idx : start_idx + shard_size, :]
534 return lora_a
535
536 def apply(self, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor:
537 return _mcp_apply(x, bias, self)
538
539 @classmethod
540 @_fully_sharded_can_replace
541 def can_replace_layer(
542 cls,
543 source_layer: nn.Module,
544 lora_config: LoRAConfig,
545 packed_modules_list: list,
546 model_config: PreTrainedConfig | None = None,
547 decorate: bool = True,
548 ) -> bool:
549 # specifying kwargs so they can be easily accessed in decorator
550 return super().can_replace_layer(
551 source_layer=source_layer,
552 lora_config=lora_config,
553 packed_modules_list=packed_modules_list,
554 model_config=model_config,
555 decorate=False,
556 )
557
558
559 class MergedColumnParallelLinearWithShardedLoRA(MergedColumnParallelLinearWithLoRA):
560 """Differs from MergedColumnParallelLinearWithLoRA by slicing the
561 LoRA A's also.
562
563 Based on S-LoRA, slicing happens along the rank dim.
564 """
565
566 def slice_lora_a(
567 self, lora_a: list[torch.Tensor | None]
568 ) -> list[torch.Tensor | None]:
569 output_shard_size = self.lora_a_stacked[0].shape[2]
570 output_start_idx = self.tp_rank * output_shard_size
571 return [
572 lora_a_i[output_start_idx : output_start_idx + output_shard_size, :]
573 if (lora_a_i := lora_a[i]) is not None
574 else None
575 for i in range(len(lora_a))
576 ]
577
578 def apply(self, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor:
579 return _mcp_apply(x, bias, self)
580
581 @classmethod
582 @_fully_sharded_can_replace
583 def can_replace_layer(
584 cls,
585 source_layer: nn.Module,
586 lora_config: LoRAConfig,
587 packed_modules_list: list,
588 model_config: PreTrainedConfig | None = None,
589 decorate: bool = True,
590 ) -> bool:
591 # specifying kwargs so they can be easily accessed in decorator
592 return super().can_replace_layer(
593 source_layer=source_layer,
594 lora_config=lora_config,
595 packed_modules_list=packed_modules_list,
596 model_config=model_config,
597 decorate=False,
598 )
599
600
601 class QKVParallelLinearWithShardedLoRA(QKVParallelLinearWithLoRA):
602 """Differs from QKVParallelLinearWithLoRA by slicing the
603 LoRA A's also.
604
605 Based on S-LoRA, slicing happens along the rank dim.
606 """
607
608 def slice_lora_a(self, lora_a: torch.Tensor) -> torch.Tensor:
609 shard_size = self.lora_a_stacked[0].shape[2]
610 start_idx = self.tp_rank * shard_size
611 lora_a = lora_a[start_idx : start_idx + shard_size, :]
612 return lora_a
613
614 def apply(self, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor:
615 return _mcp_apply(x, bias, self)
616
617 @classmethod
618 @_fully_sharded_can_replace
619 def can_replace_layer(
620 cls,
621 source_layer: nn.Module,
622 lora_config: LoRAConfig,
623 packed_modules_list: list,
624 model_config: PreTrainedConfig | None = None,
625 ) -> bool:
626 # specifying kwargs so they can be easily accessed in decorator
627 return super().can_replace_layer(
628 source_layer=source_layer,
629 lora_config=lora_config,
630 packed_modules_list=packed_modules_list,
631 model_config=model_config,
632 decorate=False,
633 )
634
635
636 class MergedQKVParallelLinearWithShardedLoRA(MergedQKVParallelLinearWithLoRA):
637 """Differs from MergedQKVParallelLinearWithLoRA by slicing the
638 LoRA A's also.
639
640 Based on S-LoRA, slicing happens along the rank dim.
641 """
642
643 def slice_lora_a(
644 self, lora_a: list[torch.Tensor | None]
645 ) -> list[torch.Tensor | None]:
646 # NOTE: lora_a contains 3 subloras, and each sublora could be None.
647 shard_size = [self.lora_a_stacked[i].shape[2] for i in range(3)]
648 start_idx = [self.tp_rank * shard_size[i] for i in range(3)]
649 lora_a = [
650 lora_a[0][start_idx[0] : start_idx[0] + shard_size[0], :]
651 if lora_a[0] is not None
652 else None,
653 lora_a[1][start_idx[1] : start_idx[1] + shard_size[1], :]
654 if lora_a[1] is not None
655 else None,
656 lora_a[2][start_idx[2] : start_idx[2] + shard_size[2], :]
657 if lora_a[2] is not None
658 else None,
659 ]
660 return lora_a
661
662 def apply(self, x: torch.Tensor, bias: torch.Tensor | None = None) -> torch.Tensor:
663 return _mcp_apply(x, bias, self)
664
665 @classmethod
666 @_fully_sharded_can_replace
667 def can_replace_layer(
668 cls,
669 source_layer: nn.Module,
670 lora_config: LoRAConfig,
671 packed_modules_list: list,
672 model_config: PreTrainedConfig | None = None,
673 decorate: bool = True,
674 ) -> bool:
675 # specifying kwargs so they can be easily accessed in decorator
676 return super().can_replace_layer(
677 source_layer=source_layer,
678 lora_config=lora_config,
679 packed_modules_list=packed_modules_list,
680 model_config=model_config,
681 decorate=False,
682 )
683
684
685 class MergedColumnParallelLinearVariableSliceWithLoRA(
686 MergedColumnParallelLinearWithLoRA
687 ):
688 """MergedColumnParallelLinear with variable number of slices (3+).
689
690 This handles cases where the checkpoint has a single weight for the whole
691 module (not split into slices), but the layer itself has multiple slices.
692 """
693
694 @classmethod
695 @_not_fully_sharded_can_replace
696 def can_replace_layer(
697 cls,
698 source_layer: nn.Module,
699 lora_config: LoRAConfig,
700 packed_modules_list: list,
701 model_config: PreTrainedConfig | None = None,
702 decorate: bool = True,
703 ) -> bool:
704 # Support MergedColumnParallelLinear with 3 or more slices
705 # (2 slices are handled by MergedColumnParallelLinearWithLoRA)
706 if not isinstance(
707 source_layer, maybe_get_oot_by_class(MergedColumnParallelLinear)
708 ):
709 return False
710
711 # If packed_modules_list has 3+ items, use this class
712 if len(packed_modules_list) >= 3:
713 return True
714
715 # If packed_modules_list has exactly 2 items, let
716 # MergedColumnParallelLinearWithLoRA handle it
717 if len(packed_modules_list) == 2:
718 return False
719
720 # If packed_modules_list is empty or has 1 item,
721 # check the layer's output_sizes.
722 # This handles cases where the checkpoint has a single weight
723 # but the layer has multiple slices (3+)
724 return (
725 hasattr(source_layer, "output_sizes")
726 and len(source_layer.output_sizes) >= 3
727 )
728
729 def set_lora(
730 self,
731 index: int,
732 lora_a: torch.Tensor | list[torch.Tensor],
733 lora_b: torch.Tensor | list[torch.Tensor],
734 ):
735 """Override to handle single tensor weights
736 that need to be split into slices."""
737 self.reset_lora(index)
738
739 # Handle case where checkpoint has single tensor weights
740 # lora_a shape: (rank, input_size) - same for all slices, duplicate it
741 if isinstance(lora_a, torch.Tensor):
742 lora_a = [lora_a] * self.n_slices
743
744 # lora_b shape: (total_output_size, rank) -
745 # split along dim 0 based on output_sizes
746 if isinstance(lora_b, torch.Tensor):
747 output_sizes = self.base_layer.output_sizes
748 lora_b_list = []
749 start_idx = 0
750 for output_size in output_sizes:
751 end_idx = start_idx + output_size
752 lora_b_list.append(lora_b[start_idx:end_idx])
753 start_idx = end_idx
754 lora_b = lora_b_list
755
756 # Now call parent's set_lora which expects lists
757 super().set_lora(index, lora_a, lora_b)