main
py 1,823 lines 63.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import random
5 from copy import deepcopy
6 from dataclasses import dataclass
7 from unittest.mock import patch
8
9 import pytest
10 import torch
11 import torch.nn.functional as F
12
13 from vllm.config.lora import LoRAConfig
14 from vllm.lora.layers import (
15 BaseLayerWithLoRA,
16 ColumnParallelLinearWithLoRA,
17 ColumnParallelLinearWithShardedLoRA,
18 LogitsProcessorWithLoRA,
19 LoRAMapping,
20 MergedColumnParallelLinearVariableSliceWithLoRA,
21 MergedColumnParallelLinearWithLoRA,
22 MergedColumnParallelLinearWithShardedLoRA,
23 MergedQKVParallelLinearWithLoRA,
24 MergedQKVParallelLinearWithShardedLoRA,
25 QKVParallelLinearWithLoRA,
26 QKVParallelLinearWithShardedLoRA,
27 ReplicatedLinearWithLoRA,
28 RowParallelLinearWithLoRA,
29 RowParallelLinearWithShardedLoRA,
30 VocabParallelEmbeddingWithLoRA,
31 )
32 from vllm.lora.lora_weights import LoRALayerWeights, PackedLoRALayerWeights
33 from vllm.lora.punica_wrapper import get_punica_wrapper
34 from vllm.model_executor.layers.fusion.quant_activation import (
35 get_input_quant_key,
36 )
37 from vllm.model_executor.layers.linear import (
38 ColumnParallelLinear,
39 MergedColumnParallelLinear,
40 QKVParallelLinear,
41 ReplicatedLinear,
42 RowParallelLinear,
43 )
44 from vllm.model_executor.layers.logits_processor import LogitsProcessor
45 from vllm.model_executor.layers.vocab_parallel_embedding import (
46 ParallelLMHead,
47 VocabParallelEmbedding,
48 get_masked_input_and_mask,
49 )
50 from vllm.model_executor.models.deepseek_v2 import DeepSeekV2FusedQkvAProjLinear
51 from vllm.platforms import current_platform
52 from vllm.utils.torch_utils import set_random_seed
53
54 from .utils import DummyLoRAManager
55
56 TOLERANCES = {
57 torch.float16: (5e-3, 5e-3),
58 torch.float32: (5e-3, 5e-3),
59 torch.bfloat16: (3e-2, 2e-2),
60 }
61
62
63 def test_lora_linear_requires_unquantized_input() -> None:
64 layer = RowParallelLinearWithLoRA.__new__(RowParallelLinearWithLoRA)
65 layer._input_quant_key = object()
66 assert get_input_quant_key(layer) is None
67
68
69 pytestmark = [
70 pytest.mark.skipif(
71 not (
72 current_platform.is_cuda_alike()
73 or current_platform.is_cpu()
74 or current_platform.is_xpu()
75 ),
76 reason="Backend not supported",
77 ),
78 # Tests here either take dist_init, which tears the distributed environment
79 # down itself, or never build one, so the global cleanup only repeats it.
80 pytest.mark.skip_global_cleanup,
81 ]
82
83 DEVICE_TYPE = current_platform.device_type
84 DEVICES = (
85 [
86 f"{DEVICE_TYPE}:{i}"
87 for i in range(1 if torch.accelerator.device_count() == 1 else 2)
88 ]
89 if (current_platform.is_cuda_alike() or current_platform.is_xpu())
90 else ["cpu"]
91 )
92
93 # prefill stage(True) or decode stage(False)
94 STAGES = [True, False]
95
96 NUM_RANDOM_SEEDS = 2
97
98 VOCAB_PARALLEL_EMBEDDING_TEST_NUM_RANDOM_SEEDS = 2
99
100
101 def test_base_layer_with_lora_delegates_load_weights():
102 class BaseLayer(torch.nn.Module):
103 def load_weights(self, weights):
104 self.loaded_weights = list(weights)
105 return {"weight"}
106
107 base_layer = BaseLayer()
108 layer = BaseLayerWithLoRA()
109 layer.base_layer = base_layer
110 weights = [("weight", torch.ones(1))]
111
112 assert layer.load_weights(weights) == {"weight"}
113 assert base_layer.loaded_weights == weights
114
115
116 @pytest.fixture(autouse=True)
117 def clean_cache_reset_device(reset_default_device):
118 # Release any memory we might be holding on to. CI runs OOMs otherwise.
119 from vllm.lora.ops.triton_ops.utils import _LORA_A_PTR_DICT, _LORA_B_PTR_DICT
120
121 _LORA_B_PTR_DICT.clear()
122 _LORA_A_PTR_DICT.clear()
123
124 yield
125
126
127 @pytest.fixture(autouse=True)
128 def skip_cuda_with_stage_false(request):
129 """On cuda-like platforms, we use the same kernels for prefill and decode
130 stage, and 'stage' is generally ignored, so we only need to test once.
131 """
132 if current_platform.is_cuda_alike() or current_platform.is_xpu():
133 try:
134 if hasattr(request.node, "callspec") and hasattr(
135 request.node.callspec, "params"
136 ):
137 params = request.node.callspec.params
138 if "stage" in params and params["stage"] is False:
139 pytest.skip("Skip test when stage=False")
140 except Exception:
141 pass
142 yield
143
144
145 def get_random_id_to_index(
146 num_loras: int, num_slots: int, log: bool = True
147 ) -> list[int | None]:
148 """Creates a random lora_id_to_index mapping.
149
150 Args:
151 num_loras: The number of active loras in the mapping.
152 num_slots: The number of slots in the mapping. Must be larger
153 than num_loras.
154 log: Whether to log the output.
155
156 """
157 if num_loras > num_slots:
158 raise ValueError(
159 f"num_loras is higher than num_slots: {num_loras} > {num_slots}. "
160 "num_loras must be less than or equal to num_slots."
161 )
162
163 slots: list[int | None] = [None] * num_slots
164 random_slot_selections = (torch.randperm(num_slots)[:num_loras]).tolist()
165 for lora_id, slot_idx in enumerate(random_slot_selections, start=1):
166 slots[slot_idx] = lora_id
167
168 if log:
169 print(f"Created lora_id_to_index mapping: {slots}.")
170
171 return slots
172
173
174 def populate_loras(
175 id_to_index: list[int | None],
176 layer: BaseLayerWithLoRA,
177 layer_weights: torch.Tensor,
178 repeats: int = 1,
179 ) -> tuple[dict[int, LoRALayerWeights], dict[int, list[LoRALayerWeights]]]:
180 """This method populates the lora layers with lora weights.
181
182 Args:
183 id_to_index: a list of lora ids. The index of the lora id
184 represents which memory slot the lora matrices are
185 stored in. A None value indicates a free slot.
186 layer: the LoRAlayer to populate.
187 layer_weights: the PyTorch tensor containing the layer's
188 weights.
189 repeats: must only be set for column parallel packed
190 layers. Indicates the number of loras to compose
191 together to create a single lora layer.
192
193 """
194 # Dictionary that maps the lora ID to the
195 # corresponding lora weights.
196 lora_dict: dict[int, LoRALayerWeights] = dict()
197
198 # Dictionary that maps the lora ID to the
199 # corresponding subloras.
200 sublora_dict: dict[int, list[LoRALayerWeights]] = dict()
201
202 for slot_idx, lora_id in enumerate(id_to_index):
203 if lora_id is not None:
204 subloras: list[LoRALayerWeights] = []
205 sublora_len = layer_weights.shape[0] // repeats
206 for i in range(repeats):
207 sublora = DummyLoRAManager(layer_weights.device).init_random_lora(
208 module_name=f"fake_{i}",
209 weight=layer_weights,
210 )
211 sublora.lora_b = sublora.lora_b[
212 (sublora_len * i) : (sublora_len * (i + 1)), :
213 ]
214 sublora.optimize()
215 subloras.append(sublora)
216
217 lora = PackedLoRALayerWeights.pack(subloras) if repeats > 1 else subloras[0]
218
219 layer.set_lora(
220 slot_idx,
221 lora_a=lora.lora_a,
222 lora_b=lora.lora_b,
223 )
224
225 lora_dict[lora_id] = lora
226 sublora_dict[lora_id] = subloras
227
228 return lora_dict, sublora_dict
229
230
231 def create_random_inputs(
232 active_lora_ids: list[int],
233 num_inputs: int,
234 input_size: tuple[int, ...],
235 input_range: tuple[float, float],
236 input_type: torch.dtype = torch.int,
237 device: torch.device = DEVICE_TYPE,
238 ) -> tuple[list[torch.Tensor], list[int], list[int]]:
239 """Creates random inputs.
240
241 Args:
242 active_lora_ids: lora IDs of active lora weights.
243 num_inputs: the number of inputs to create.
244 input_size: the size of each individual input.
245 input_range: the range of values to include in the input.
246 input_range[0] <= possible input values < input_range[1]
247 input_type: the type of values in the input.
248
249 """
250 low, high = input_range
251
252 inputs: list[torch.Tensor] = []
253 index_mapping: list[int] = []
254 prompt_mapping: list[int] = []
255
256 for _ in range(num_inputs):
257 if input_type == torch.int:
258 inputs.append(
259 torch.randint(
260 low=int(low), high=int(high), size=input_size, device=device
261 )
262 )
263 else:
264 inputs.append(
265 torch.rand(size=input_size, dtype=input_type, device=device) * high
266 + low
267 )
268
269 lora_id = random.choice(active_lora_ids)
270 index_mapping += [lora_id] * input_size[0]
271 prompt_mapping += [lora_id]
272
273 return inputs, index_mapping, prompt_mapping
274
275
276 def check_punica_wrapper(punica_wrapper) -> bool:
277 if current_platform.is_cuda_alike():
278 from vllm.lora.punica_wrapper.punica_gpu import PunicaWrapperGPU
279
280 return type(punica_wrapper) is PunicaWrapperGPU
281 elif current_platform.is_cpu():
282 from vllm.lora.punica_wrapper.punica_cpu import PunicaWrapperCPU
283
284 return type(punica_wrapper) is PunicaWrapperCPU
285 elif current_platform.is_xpu():
286 from vllm.lora.punica_wrapper.punica_xpu import PunicaWrapperXPU
287
288 return type(punica_wrapper) is PunicaWrapperXPU
289 else:
290 return False
291
292
293 @torch.inference_mode()
294 @pytest.mark.parametrize("num_loras", [1, 2, 4])
295 @pytest.mark.parametrize("device", DEVICES)
296 @pytest.mark.parametrize("vocab_size", [512, 32000, 64000, 128000])
297 @pytest.mark.parametrize("stage", STAGES)
298 def test_embeddings(
299 default_vllm_config, dist_init, num_loras, device, vocab_size, stage
300 ) -> None:
301 # For multi-GPU testing of Triton kernel, we must explicitly set the CUDA
302 # device, see: https://github.com/triton-lang/triton/issues/2925
303 # Same below.
304 if current_platform.is_cuda_alike() or current_platform.is_xpu():
305 torch.accelerator.set_device_index(device)
306
307 torch.set_default_device(device)
308 max_loras = 8
309 lora_config = LoRAConfig(
310 max_loras=max_loras, max_lora_rank=8, lora_dtype=torch.float16
311 )
312 punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
313 assert check_punica_wrapper(punica_wrapper)
314
315 def create_random_embedding_layer():
316 embedding = VocabParallelEmbedding(vocab_size, 256)
317 embedding.weight.data = torch.rand_like(embedding.weight.data)
318 embedding.weight.data[vocab_size:, :] = 0
319 lora_embedding = VocabParallelEmbeddingWithLoRA(embedding)
320 lora_embedding.create_lora_weights(max_loras, lora_config)
321
322 return embedding, lora_embedding
323
324 for i in range(NUM_RANDOM_SEEDS):
325 set_random_seed(i)
326
327 id_to_index = get_random_id_to_index(num_loras, max_loras)
328 embedding, lora_embedding = create_random_embedding_layer()
329 lora_embedding.set_mapping(punica_wrapper)
330 lora_dict, _ = populate_loras(
331 id_to_index,
332 layer=lora_embedding,
333 layer_weights=embedding.weight.T,
334 )
335
336 inputs, index_mapping, prompt_mapping = create_random_inputs(
337 active_lora_ids=list(lora_dict.keys()),
338 num_inputs=num_loras * 3,
339 input_size=(200,),
340 input_range=(1, vocab_size),
341 device=device,
342 )
343 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
344 punica_wrapper.update_metadata(
345 lora_mapping,
346 id_to_index,
347 max_loras,
348 vocab_size,
349 )
350
351 lora_result = lora_embedding(torch.cat(inputs))
352
353 expected_results: list[torch.Tensor] = []
354 for input_, lora_id in zip(inputs, prompt_mapping):
355 lora = lora_dict[lora_id]
356 result = embedding(input_)
357 after_a = F.embedding(
358 input_,
359 lora.lora_a.T,
360 )
361 result += after_a @ lora.lora_b.T
362 expected_results.append(result)
363 expected_result = torch.cat(expected_results)
364
365 rtol, atol = TOLERANCES[lora_result.dtype]
366 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
367
368 # Check that resetting the lora weights succeeds
369
370 for slot_idx in range(max_loras):
371 lora_embedding.reset_lora(slot_idx)
372
373 inputs, index_mapping, prompt_mapping = create_random_inputs(
374 active_lora_ids=[0],
375 num_inputs=num_loras * 3,
376 input_size=(200,),
377 input_range=(1, vocab_size),
378 device=device,
379 )
380 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
381 punica_wrapper.update_metadata(
382 lora_mapping,
383 id_to_index,
384 max_loras,
385 vocab_size,
386 )
387
388 lora_result = lora_embedding(torch.cat(inputs))
389 expected_result = embedding(torch.cat(inputs))
390
391 rtol, atol = TOLERANCES[lora_result.dtype]
392 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
393
394
395 @torch.inference_mode()
396 @pytest.mark.parametrize("num_loras", [1, 2, 4])
397 @pytest.mark.parametrize("device", DEVICES)
398 @pytest.mark.parametrize("vocab_size", [64000, 256512, 258048])
399 @pytest.mark.parametrize("stage", STAGES)
400 @pytest.mark.parametrize("head_dtype", [None, torch.float32])
401 def test_lm_head_logits_processor(
402 default_vllm_config, dist_init, num_loras, device, vocab_size, stage, head_dtype
403 ) -> None:
404 if current_platform.is_cuda_alike() or current_platform.is_xpu():
405 torch.accelerator.set_device_index(device)
406
407 torch.set_default_device(device)
408 max_loras = 8
409 lora_config = LoRAConfig(
410 max_loras=max_loras, max_lora_rank=8, lora_dtype=torch.float16
411 )
412 punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
413 assert check_punica_wrapper(punica_wrapper)
414
415 def _pretest():
416 linear = ParallelLMHead(
417 num_embeddings=vocab_size,
418 embedding_dim=1024,
419 params_dtype=torch.float16,
420 )
421 linear.weight.data = torch.rand_like(linear.weight.data)
422 linear.weight.data[:, vocab_size:] = 0
423 logits_processor = LogitsProcessor(vocab_size)
424 # Exercise an fp32 lm_head (head_dtype) on the LoRA path.
425 logits_processor.head_dtype = head_dtype
426 lora_logits_processor = LogitsProcessorWithLoRA(
427 logits_processor, 1024, linear.weight.dtype, linear.weight.device, None
428 )
429 lora_logits_processor.create_lora_weights(max_loras, lora_config)
430
431 return linear, logits_processor, lora_logits_processor
432
433 for i in range(NUM_RANDOM_SEEDS):
434 set_random_seed(i)
435
436 id_to_index = get_random_id_to_index(num_loras, max_loras)
437 linear, logits_processor, lora_logits_processor = _pretest()
438 lora_logits_processor.set_mapping(punica_wrapper)
439
440 lora_dict, _ = populate_loras(
441 id_to_index,
442 layer=lora_logits_processor,
443 layer_weights=linear.weight,
444 )
445
446 inputs, index_mapping, prompt_mapping = create_random_inputs(
447 active_lora_ids=list(lora_dict.keys()),
448 num_inputs=8 * num_loras, # * 3,
449 input_size=(1, 1024),
450 input_range=(0, 1),
451 input_type=torch.float16,
452 device=device,
453 )
454 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
455 punica_wrapper.update_metadata(
456 lora_mapping,
457 id_to_index,
458 max_loras,
459 vocab_size,
460 )
461 input_ = torch.rand(20, 1024)
462
463 lora_result = lora_logits_processor._get_logits(
464 hidden_states=torch.cat(inputs), lm_head=linear, embedding_bias=None
465 )
466
467 original_lm_head = deepcopy(linear)
468
469 expected_results: list[torch.Tensor] = []
470 for input_, lora_id in zip(inputs, prompt_mapping):
471 lora = lora_dict[lora_id]
472 result = logits_processor._get_logits(
473 hidden_states=input_, lm_head=linear, embedding_bias=None
474 )
475
476 result += input_ @ lora.lora_a.T @ lora.lora_b.T * lora.scaling
477 expected_results.append(result)
478 expected_result = torch.cat(expected_results)
479
480 # Check that resetting the lora weights succeeds
481
482 for slot_idx in range(max_loras):
483 lora_logits_processor.reset_lora(slot_idx)
484
485 inputs, index_mapping, prompt_mapping = create_random_inputs(
486 active_lora_ids=[0],
487 num_inputs=8 * num_loras * 3,
488 input_size=(1, 1024),
489 input_range=(0, 1),
490 input_type=torch.float16,
491 device=device,
492 )
493 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
494 punica_wrapper.update_metadata(
495 lora_mapping,
496 id_to_index,
497 max_loras,
498 vocab_size,
499 )
500
501 lora_result = lora_logits_processor._get_logits(
502 hidden_states=torch.cat(inputs),
503 lm_head=original_lm_head,
504 embedding_bias=None,
505 )[:, :vocab_size]
506 expected_result = logits_processor._get_logits(
507 hidden_states=torch.cat(inputs),
508 lm_head=original_lm_head,
509 embedding_bias=None,
510 )
511
512 rtol, atol = TOLERANCES[lora_result.dtype]
513 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
514
515
516 @torch.inference_mode()
517 @pytest.mark.parametrize("vocab_size", [258049, 300000])
518 @pytest.mark.parametrize("device", DEVICES)
519 def test_lm_head_logits_processor_invalid_vocab_size(
520 default_vllm_config, dist_init, vocab_size, device
521 ) -> None:
522 """Test that LogitsProcessorWithLoRA raises ValueError for invalid vocab sizes."""
523 if current_platform.is_cuda_alike() or current_platform.is_xpu():
524 torch.accelerator.set_device_index(device)
525
526 torch.set_default_device(device)
527 max_loras = 8
528 lora_config = LoRAConfig(
529 max_loras=max_loras, max_lora_rank=8, lora_dtype=torch.float16
530 )
531
532 logits_processor = LogitsProcessor(vocab_size)
533 lora_logits_processor = LogitsProcessorWithLoRA(
534 logits_processor, 1024, torch.float16, device, None
535 )
536
537 with pytest.raises(ValueError, match="vocab size must be <= 258048"):
538 lora_logits_processor.create_lora_weights(max_loras, lora_config)
539
540
541 @torch.inference_mode()
542 @pytest.mark.parametrize("device", DEVICES)
543 def test_lm_head_reset_sharded_to_full_mapping(
544 default_vllm_config, dist_init, device
545 ) -> None:
546 if current_platform.is_cuda_alike() or current_platform.is_xpu():
547 torch.accelerator.set_device_index(device)
548
549 torch.set_default_device(device)
550 max_loras = 8
551 vocab_size = 1024
552 lora_config = LoRAConfig(
553 max_loras=max_loras, max_lora_rank=8, lora_dtype=torch.float16
554 )
555
556 logits_processor = LogitsProcessor(vocab_size)
557 sharded_to_full_mapping = list(reversed(range(vocab_size)))
558 lora_logits_processor = LogitsProcessorWithLoRA(
559 logits_processor, 1024, torch.float16, device, sharded_to_full_mapping
560 )
561 lora_logits_processor.create_lora_weights(max_loras, lora_config)
562
563 lora_logits_processor.sharded_to_full_mapping_gpu.fill_(-1)
564
565 lora_logits_processor.reset_sharded_to_full_mapping()
566
567 torch.testing.assert_close(
568 lora_logits_processor.sharded_to_full_mapping_gpu,
569 torch.tensor(sharded_to_full_mapping, dtype=torch.long),
570 )
571
572
573 @torch.inference_mode()
574 @pytest.mark.parametrize("num_loras", [1, 2, 4])
575 @pytest.mark.parametrize("device", DEVICES)
576 @pytest.mark.parametrize("stage", STAGES)
577 def test_linear_replicated(
578 default_vllm_config,
579 dist_init,
580 num_loras,
581 device,
582 stage,
583 ) -> None:
584 if current_platform.is_cuda_alike() or current_platform.is_xpu():
585 torch.accelerator.set_device_index(device)
586
587 max_loras = 8
588 torch.set_default_device(device)
589 lora_config = LoRAConfig(
590 max_loras=max_loras,
591 max_lora_rank=8,
592 lora_dtype=torch.float16,
593 )
594 punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
595 assert check_punica_wrapper(punica_wrapper)
596
597 def create_random_linear_replicated_layer(idx: int = 0):
598 linear = ReplicatedLinear(
599 4096, 4096, bias=False, params_dtype=torch.float16, prefix=f"layer_{idx}"
600 )
601 linear.weight.data = torch.rand_like(linear.weight.data)
602 lora_linear = ReplicatedLinearWithLoRA(linear)
603
604 lora_linear.create_lora_weights(max_loras, lora_config)
605 assert (
606 lora_linear.n_slices
607 == len(lora_linear.lora_a_stacked)
608 == len(lora_linear.lora_b_stacked)
609 == 1
610 )
611 return linear, lora_linear
612
613 for i in range(NUM_RANDOM_SEEDS):
614 set_random_seed(i)
615
616 id_to_index = get_random_id_to_index(num_loras, max_loras)
617 linear, lora_linear = create_random_linear_replicated_layer(i)
618 assert torch.equal(linear.weight, lora_linear.weight)
619 lora_linear.set_mapping(punica_wrapper)
620 lora_dict, _ = populate_loras(
621 id_to_index,
622 layer=lora_linear,
623 layer_weights=linear.weight,
624 )
625
626 inputs, index_mapping, prompt_mapping = create_random_inputs(
627 active_lora_ids=list(lora_dict.keys()),
628 num_inputs=32 * num_loras,
629 input_size=(1, 4096),
630 input_range=(0, 1),
631 input_type=torch.float16,
632 device=device,
633 )
634 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
635 punica_wrapper.update_metadata(
636 lora_mapping,
637 id_to_index,
638 max_loras,
639 512,
640 )
641
642 lora_result = lora_linear(torch.cat(inputs))[0]
643
644 expected_results: list[torch.Tensor] = []
645 for input_, lora_id in zip(inputs, prompt_mapping):
646 lora = lora_dict[lora_id]
647 result = linear(input_)[0]
648 result += input_ @ lora.lora_a.T @ lora.lora_b.T * lora.scaling
649 expected_results.append(result)
650 expected_result = torch.cat(expected_results)
651
652 rtol, atol = TOLERANCES[lora_result.dtype]
653 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
654
655 # Check that resetting the lora weights succeeds
656
657 for slot_idx in range(max_loras):
658 lora_linear.reset_lora(slot_idx)
659
660 inputs, index_mapping, prompt_mapping = create_random_inputs(
661 active_lora_ids=[0],
662 num_inputs=32 * num_loras,
663 input_size=(1, 4096),
664 input_range=(0, 1),
665 input_type=torch.float16,
666 device=device,
667 )
668 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
669
670 punica_wrapper.update_metadata(
671 lora_mapping,
672 id_to_index,
673 max_loras,
674 512,
675 )
676
677 lora_result = lora_linear(torch.cat(inputs))[0]
678 expected_result = linear(torch.cat(inputs))[0]
679
680 rtol, atol = TOLERANCES[lora_result.dtype]
681 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
682
683
684 @torch.inference_mode()
685 @pytest.mark.parametrize("num_loras", [1, 2, 4])
686 @pytest.mark.parametrize("orientation", ["row", "column"])
687 @pytest.mark.parametrize("fully_shard", [True, False])
688 @pytest.mark.parametrize("device", DEVICES)
689 @pytest.mark.parametrize("stage", STAGES)
690 def test_linear_parallel(
691 default_vllm_config, dist_init, num_loras, orientation, fully_shard, device, stage
692 ) -> None:
693 if current_platform.is_cuda_alike() or current_platform.is_xpu():
694 torch.accelerator.set_device_index(device)
695
696 max_loras = 8
697 torch.set_default_device(device)
698 lora_config = LoRAConfig(
699 max_loras=max_loras,
700 max_lora_rank=8,
701 fully_sharded_loras=fully_shard,
702 lora_dtype=torch.float16,
703 )
704 punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
705 assert check_punica_wrapper(punica_wrapper)
706
707 def create_random_linear_parallel_layer(idx: int = 0):
708 if orientation == "row":
709 linear = RowParallelLinear(
710 4096,
711 4096,
712 bias=False,
713 params_dtype=torch.float16,
714 prefix=f"layer_{idx}",
715 )
716 linear.weight.data = torch.rand_like(linear.weight.data)
717 lora_linear = (
718 RowParallelLinearWithLoRA(linear)
719 if not fully_shard
720 else RowParallelLinearWithShardedLoRA(linear)
721 )
722 else:
723 linear = ColumnParallelLinear(
724 4096,
725 4096,
726 bias=False,
727 params_dtype=torch.float16,
728 prefix=f"layer_{idx}",
729 )
730 linear.weight.data = torch.rand_like(linear.weight.data)
731 lora_linear = (
732 ColumnParallelLinearWithLoRA(linear)
733 if not fully_shard
734 else ColumnParallelLinearWithShardedLoRA(linear)
735 )
736 lora_linear.create_lora_weights(max_loras, lora_config)
737 assert (
738 lora_linear.n_slices
739 == len(lora_linear.lora_a_stacked)
740 == len(lora_linear.lora_b_stacked)
741 == 1
742 )
743
744 return linear, lora_linear
745
746 for i in range(NUM_RANDOM_SEEDS):
747 set_random_seed(i)
748
749 id_to_index = get_random_id_to_index(num_loras, max_loras)
750 linear, lora_linear = create_random_linear_parallel_layer(i)
751 assert torch.equal(linear.weight, lora_linear.weight)
752 lora_linear.set_mapping(punica_wrapper)
753 lora_dict, _ = populate_loras(
754 id_to_index,
755 layer=lora_linear,
756 layer_weights=linear.weight,
757 )
758
759 inputs, index_mapping, prompt_mapping = create_random_inputs(
760 active_lora_ids=list(lora_dict.keys()),
761 num_inputs=32 * num_loras,
762 input_size=(1, 4096),
763 input_range=(0, 1),
764 input_type=torch.float16,
765 device=device,
766 )
767 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
768 punica_wrapper.update_metadata(
769 lora_mapping,
770 id_to_index,
771 max_loras,
772 512,
773 )
774
775 lora_result = lora_linear(torch.cat(inputs))[0]
776
777 expected_results: list[torch.Tensor] = []
778 for input_, lora_id in zip(inputs, prompt_mapping):
779 lora = lora_dict[lora_id]
780 result = linear(input_)[0]
781 result += input_ @ lora.lora_a.T @ lora.lora_b.T * lora.scaling
782 expected_results.append(result)
783 expected_result = torch.cat(expected_results)
784
785 rtol, atol = TOLERANCES[lora_result.dtype]
786 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
787
788 # Check that resetting the lora weights succeeds
789
790 for slot_idx in range(max_loras):
791 lora_linear.reset_lora(slot_idx)
792
793 inputs, index_mapping, prompt_mapping = create_random_inputs(
794 active_lora_ids=[0],
795 num_inputs=32 * num_loras,
796 input_size=(1, 4096),
797 input_range=(0, 1),
798 input_type=torch.float16,
799 device=device,
800 )
801 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
802
803 punica_wrapper.update_metadata(
804 lora_mapping,
805 id_to_index,
806 max_loras,
807 512,
808 )
809
810 lora_result = lora_linear(torch.cat(inputs))[0]
811 expected_result = linear(torch.cat(inputs))[0]
812
813 rtol, atol = TOLERANCES[lora_result.dtype]
814 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
815
816
817 @torch.inference_mode()
818 @pytest.mark.parametrize("num_loras", [1, 2, 4])
819 @pytest.mark.parametrize("repeats", [1, 2, 3])
820 @pytest.mark.parametrize("fully_shard", [True, False])
821 @pytest.mark.parametrize("device", DEVICES)
822 @pytest.mark.parametrize("stage", STAGES)
823 def test_column_parallel_packed(
824 default_vllm_config, dist_init, num_loras, repeats, fully_shard, device, stage
825 ) -> None:
826 if current_platform.is_cuda_alike() or current_platform.is_xpu():
827 torch.accelerator.set_device_index(device)
828
829 max_loras = 8
830 torch.set_default_device(device)
831 lora_config = LoRAConfig(
832 max_loras=max_loras,
833 max_lora_rank=8,
834 fully_sharded_loras=fully_shard,
835 lora_dtype=torch.float16,
836 )
837 punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
838 assert check_punica_wrapper(punica_wrapper)
839
840 def create_column_parallel_packed_layer(idx: int = 0):
841 if repeats == 2:
842 linear = MergedColumnParallelLinear(
843 4096,
844 [4096] * repeats,
845 bias=False,
846 params_dtype=torch.float16,
847 prefix=f"layer_{idx}",
848 )
849 linear.weight.data = torch.rand_like(linear.weight.data)
850 lora_linear = (
851 MergedColumnParallelLinearWithLoRA(linear)
852 if not fully_shard
853 else MergedColumnParallelLinearWithShardedLoRA(linear)
854 )
855 elif repeats == 3:
856 linear = QKVParallelLinear(
857 4096,
858 64,
859 32,
860 bias=False,
861 params_dtype=torch.float16,
862 prefix=f"layer_{idx}",
863 )
864 linear.weight.data = torch.rand_like(linear.weight.data)
865 lora_linear = (
866 MergedQKVParallelLinearWithLoRA(linear)
867 if not fully_shard
868 else MergedQKVParallelLinearWithShardedLoRA(linear)
869 )
870 else:
871 linear = QKVParallelLinear(
872 4096,
873 64,
874 32,
875 bias=False,
876 params_dtype=torch.float16,
877 prefix=f"layer_{idx}",
878 )
879 linear.weight.data = torch.rand_like(linear.weight.data)
880 lora_linear = (
881 QKVParallelLinearWithLoRA(linear)
882 if not fully_shard
883 else QKVParallelLinearWithShardedLoRA(linear)
884 )
885
886 @dataclass
887 class FakeConfig:
888 hidden_size = 4096
889 num_key_value_heads = 32
890 num_attention_heads = 32
891
892 n_slices = repeats
893 lora_linear.create_lora_weights(
894 max_loras, lora_config, model_config=FakeConfig()
895 )
896 assert (
897 lora_linear.n_slices
898 == len(lora_linear.lora_a_stacked)
899 == len(lora_linear.lora_b_stacked)
900 == n_slices
901 )
902
903 return linear, lora_linear
904
905 for i in range(NUM_RANDOM_SEEDS):
906 set_random_seed(i)
907
908 id_to_index = get_random_id_to_index(num_loras, max_loras)
909
910 linear, lora_linear = create_column_parallel_packed_layer(i)
911 assert torch.equal(linear.weight, lora_linear.weight)
912 lora_linear.set_mapping(punica_wrapper)
913 lora_dict, sublora_dict = populate_loras(
914 id_to_index,
915 layer=lora_linear,
916 layer_weights=linear.weight,
917 repeats=repeats,
918 )
919
920 inputs, index_mapping, prompt_mapping = create_random_inputs(
921 active_lora_ids=list(lora_dict.keys()),
922 num_inputs=32 * num_loras,
923 input_size=(1, 4096),
924 input_range=(0, 1),
925 input_type=torch.float16,
926 device=device,
927 )
928 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
929
930 punica_wrapper.update_metadata(
931 lora_mapping,
932 id_to_index,
933 max_loras,
934 512,
935 )
936
937 lora_result = lora_linear(torch.cat(inputs))[0]
938
939 expected_results: list[torch.Tensor] = []
940 for input_, lora_id in zip(inputs, prompt_mapping):
941 result = linear(input_)[0]
942 subloras = sublora_dict[lora_id]
943 for i, sublora in enumerate(subloras):
944 result[
945 :, sublora.lora_b.shape[0] * i : sublora.lora_b.shape[0] * (i + 1)
946 ] += input_ @ sublora.lora_a.T @ sublora.lora_b.T * sublora.scaling
947 expected_results.append(result)
948 expected_result = torch.cat(expected_results)
949
950 rtol, atol = TOLERANCES[lora_result.dtype]
951 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
952
953 for slot_idx in range(max_loras):
954 lora_linear.reset_lora(slot_idx)
955
956 inputs, index_mapping, prompt_mapping = create_random_inputs(
957 active_lora_ids=[0],
958 num_inputs=32 * num_loras,
959 input_size=(1, 4096),
960 input_range=(0, 1),
961 input_type=torch.float16,
962 device=device,
963 )
964 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
965
966 punica_wrapper.update_metadata(
967 lora_mapping,
968 id_to_index,
969 max_loras,
970 512,
971 )
972
973 lora_result = lora_linear(torch.cat(inputs))[0]
974 expected_result = linear(torch.cat(inputs))[0]
975
976 rtol, atol = TOLERANCES[lora_result.dtype]
977 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
978
979
980 @torch.inference_mode()
981 @pytest.mark.parametrize("num_loras", [1, 2, 4])
982 @pytest.mark.parametrize("num_slices", [3, 5])
983 @pytest.mark.parametrize("device", DEVICES)
984 @pytest.mark.parametrize("stage", STAGES)
985 def test_merged_column_parallel_variable_slice(
986 default_vllm_config, dist_init, num_loras, num_slices, device, stage
987 ) -> None:
988 if current_platform.is_cuda_alike() or current_platform.is_xpu():
989 torch.accelerator.set_device_index(device)
990
991 max_loras = 8
992 torch.set_default_device(device)
993 lora_config = LoRAConfig(
994 max_loras=max_loras, max_lora_rank=8, lora_dtype=torch.float16
995 )
996 punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
997
998 # Set number of output slices
999 output_sizes = [1024 + i * 256 for i in range(num_slices)]
1000 total_output = sum(output_sizes)
1001
1002 def create_layer(idx: int = 0):
1003 # Create linear layer
1004 linear = MergedColumnParallelLinear(
1005 4096,
1006 output_sizes,
1007 bias=False,
1008 params_dtype=torch.float16,
1009 prefix=f"layer_{idx}",
1010 )
1011 linear.weight.data = torch.rand_like(linear.weight.data)
1012
1013 # Create linear layer with LoRA adapter
1014 lora_linear = MergedColumnParallelLinearVariableSliceWithLoRA(linear)
1015 lora_linear.create_lora_weights(max_loras, lora_config)
1016 return linear, lora_linear
1017
1018 for i in range(NUM_RANDOM_SEEDS):
1019 set_random_seed(i)
1020 id_to_index = get_random_id_to_index(num_loras, max_loras)
1021 linear, lora_linear = create_layer(i)
1022 lora_linear.set_mapping(punica_wrapper)
1023
1024 # Populate LoRA weights
1025 lora_dict, sublora_dict = {}, {}
1026 for slot_idx, lora_id in enumerate(id_to_index):
1027 if lora_id is not None:
1028 # Create random LoRA weights
1029 lora_a = torch.rand(8, 4096, dtype=torch.float16, device=device)
1030 lora_b = torch.rand(total_output, 8, dtype=torch.float16, device=device)
1031 lora_linear.set_lora(slot_idx, lora_a, lora_b)
1032 lora_dict[lora_id] = (lora_a, lora_b)
1033
1034 # Split lora_b for expected computation
1035 sublora_dict[lora_id] = torch.split(lora_b, output_sizes, dim=0)
1036
1037 inputs, index_mapping, prompt_mapping = create_random_inputs(
1038 active_lora_ids=list(lora_dict.keys()),
1039 num_inputs=32 * num_loras,
1040 input_size=(1, 4096),
1041 input_range=(0, 1),
1042 input_type=torch.float16,
1043 device=device,
1044 )
1045 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
1046 punica_wrapper.update_metadata(lora_mapping, id_to_index, max_loras, 512)
1047
1048 # Compute LoRA result
1049 lora_result = lora_linear(torch.cat(inputs))[0]
1050
1051 # Compute expected result
1052 expected_results = []
1053 for input_, lora_id in zip(inputs, prompt_mapping):
1054 result = linear(input_)[0]
1055 lora_a, _ = lora_dict[lora_id]
1056 offset = 0
1057 # Compute expected result for each sublora
1058 for lora_b_slice in sublora_dict[lora_id]:
1059 sz = lora_b_slice.shape[0]
1060 result[:, offset : offset + sz] += input_ @ lora_a.T @ lora_b_slice.T
1061 offset += sz
1062 expected_results.append(result)
1063
1064 # Check that the LoRA result is close to the expected result
1065 rtol, atol = TOLERANCES[lora_result.dtype]
1066 torch.testing.assert_close(
1067 lora_result, torch.cat(expected_results), rtol=rtol, atol=atol
1068 )
1069
1070 # Reset LoRA weights and check results with zero LoRA weights
1071 for slot_idx in range(max_loras):
1072 lora_linear.reset_lora(slot_idx)
1073
1074 inputs, index_mapping, prompt_mapping = create_random_inputs(
1075 active_lora_ids=[0],
1076 num_inputs=32 * num_loras,
1077 input_size=(1, 4096),
1078 input_range=(0, 1),
1079 input_type=torch.float16,
1080 device=device,
1081 )
1082 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
1083 punica_wrapper.update_metadata(lora_mapping, id_to_index, max_loras, 512)
1084
1085 # After resetting LoRA weights,
1086 # lora_linear should behave like the base linear layer
1087 lora_result = lora_linear(torch.cat(inputs))[0]
1088 expected_result = linear(torch.cat(inputs))[0]
1089
1090 rtol, atol = TOLERANCES[lora_result.dtype]
1091 torch.testing.assert_close(lora_result, expected_result, rtol=rtol, atol=atol)
1092
1093
1094 @pytest.mark.parametrize("tp_size", [1, 2, 4, 8])
1095 @pytest.mark.parametrize(
1096 "seed", list(range(VOCAB_PARALLEL_EMBEDDING_TEST_NUM_RANDOM_SEEDS))
1097 )
1098 def test_vocab_parallel_embedding_indices(tp_size, seed, default_vllm_config):
1099 random.seed(seed)
1100 vocab_size = random.randint(4000, 64000)
1101 added_vocab_size = random.randint(0, 1024)
1102 org_vocab_size = vocab_size - added_vocab_size
1103 last_org_vocab_end_index = 0
1104 last_added_vocab_end_index = org_vocab_size
1105 computed_vocab_size = 0
1106 computed_org_vocab_size = 0
1107 computed_added_vocab_size = 0
1108 vocab_size_padded = -1
1109
1110 all_org_tokens: list[int] = []
1111 all_added_tokens: list[int] = []
1112 token_ids: list[int] = []
1113
1114 for tp_rank in range(tp_size):
1115 with (
1116 patch(
1117 "vllm.model_executor.layers.vocab_parallel_embedding.get_tensor_model_parallel_rank",
1118 return_value=tp_rank,
1119 ),
1120 patch(
1121 "vllm.model_executor.layers.vocab_parallel_embedding.get_tensor_model_parallel_world_size",
1122 return_value=tp_size,
1123 ),
1124 ):
1125 vocab_embedding = VocabParallelEmbedding(
1126 vocab_size, 1, org_num_embeddings=org_vocab_size
1127 )
1128 vocab_size_padded = vocab_embedding.num_embeddings_padded
1129 shard_indices = vocab_embedding.shard_indices
1130 # Assert that the ranges are contiguous
1131 assert shard_indices.org_vocab_start_index == last_org_vocab_end_index
1132 assert shard_indices.added_vocab_start_index == last_added_vocab_end_index
1133
1134 # Ensure that we are not exceeding the vocab size
1135 computed_vocab_size += shard_indices.num_elements_padded
1136 computed_org_vocab_size += shard_indices.num_org_elements
1137 computed_added_vocab_size += shard_indices.num_added_elements
1138
1139 # Ensure that the ranges are not overlapping
1140 all_org_tokens.extend(
1141 range(
1142 shard_indices.org_vocab_start_index, shard_indices.org_vocab_end_index
1143 )
1144 )
1145 all_added_tokens.extend(
1146 range(
1147 shard_indices.added_vocab_start_index,
1148 shard_indices.added_vocab_end_index,
1149 )
1150 )
1151
1152 token_ids.extend(
1153 range(
1154 shard_indices.org_vocab_start_index, shard_indices.org_vocab_end_index
1155 )
1156 )
1157 token_ids.extend(
1158 [-1]
1159 * (shard_indices.num_org_elements_padded - shard_indices.num_org_elements)
1160 )
1161 token_ids.extend(
1162 range(
1163 shard_indices.added_vocab_start_index,
1164 shard_indices.added_vocab_end_index,
1165 )
1166 )
1167 token_ids.extend(
1168 [-1]
1169 * (
1170 shard_indices.num_added_elements_padded
1171 - shard_indices.num_added_elements
1172 )
1173 )
1174
1175 last_org_vocab_end_index = shard_indices.org_vocab_end_index
1176 last_added_vocab_end_index = shard_indices.added_vocab_end_index
1177
1178 assert computed_vocab_size == vocab_size_padded
1179 assert computed_org_vocab_size == org_vocab_size
1180 assert computed_added_vocab_size == added_vocab_size
1181
1182 # Ensure that the ranges are not overlapping
1183 assert len(all_org_tokens) == len(set(all_org_tokens))
1184 assert len(all_added_tokens) == len(set(all_added_tokens))
1185 assert not set(all_org_tokens).intersection(set(all_added_tokens))
1186
1187 token_ids_tensor = torch.tensor(token_ids, dtype=torch.long)
1188 reindex_mapping = vocab_embedding.get_sharded_to_full_mapping()
1189 assert reindex_mapping is not None or tp_size == 1
1190 if reindex_mapping is not None:
1191 reindexed_token_ids = token_ids_tensor[reindex_mapping]
1192 expected = torch.tensor(list(range(0, vocab_size)))
1193 assert reindexed_token_ids[:vocab_size].equal(expected)
1194 assert torch.all(reindexed_token_ids[vocab_size:] == -1)
1195
1196
1197 def test_get_masked_input_and_mask():
1198 x = torch.tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
1199
1200 # base tp 1 case, no padding
1201 modified_x, _ = get_masked_input_and_mask(
1202 x,
1203 org_vocab_start_index=0,
1204 org_vocab_end_index=8,
1205 added_vocab_start_index=8,
1206 added_vocab_end_index=12,
1207 num_org_vocab_padding=0,
1208 )
1209 assert torch.equal(x, modified_x)
1210
1211 # tp 2 case, no padding
1212 modified_x_rank_0, _ = get_masked_input_and_mask(
1213 x,
1214 org_vocab_start_index=0,
1215 org_vocab_end_index=4,
1216 added_vocab_start_index=8,
1217 added_vocab_end_index=10,
1218 num_org_vocab_padding=0,
1219 )
1220 modified_x_rank_1, _ = get_masked_input_and_mask(
1221 x,
1222 org_vocab_start_index=4,
1223 org_vocab_end_index=8,
1224 added_vocab_start_index=10,
1225 added_vocab_end_index=12,
1226 num_org_vocab_padding=0,
1227 )
1228 assert torch.equal(
1229 modified_x_rank_0, torch.tensor([0, 1, 2, 3, 0, 0, 0, 0, 4, 5, 0, 0])
1230 )
1231 assert torch.equal(
1232 modified_x_rank_1, torch.tensor([0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 4, 5])
1233 )
1234
1235 # tp 4 case, no padding
1236 modified_x_rank_0, _ = get_masked_input_and_mask(
1237 x,
1238 org_vocab_start_index=0,
1239 org_vocab_end_index=2,
1240 added_vocab_start_index=8,
1241 added_vocab_end_index=9,
1242 num_org_vocab_padding=0,
1243 )
1244 modified_x_rank_1, _ = get_masked_input_and_mask(
1245 x,
1246 org_vocab_start_index=2,
1247 org_vocab_end_index=4,
1248 added_vocab_start_index=9,
1249 added_vocab_end_index=10,
1250 num_org_vocab_padding=0,
1251 )
1252 modified_x_rank_2, _ = get_masked_input_and_mask(
1253 x,
1254 org_vocab_start_index=4,
1255 org_vocab_end_index=6,
1256 added_vocab_start_index=10,
1257 added_vocab_end_index=11,
1258 num_org_vocab_padding=0,
1259 )
1260 modified_x_rank_3, _ = get_masked_input_and_mask(
1261 x,
1262 org_vocab_start_index=6,
1263 org_vocab_end_index=8,
1264 added_vocab_start_index=11,
1265 added_vocab_end_index=12,
1266 num_org_vocab_padding=0,
1267 )
1268 assert torch.equal(
1269 modified_x_rank_0, torch.tensor([0, 1, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0])
1270 )
1271 assert torch.equal(
1272 modified_x_rank_1, torch.tensor([0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0])
1273 )
1274 assert torch.equal(
1275 modified_x_rank_2, torch.tensor([0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0])
1276 )
1277 assert torch.equal(
1278 modified_x_rank_3, torch.tensor([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2])
1279 )
1280
1281 # base tp 1 case, with padding
1282 modified_x, _ = get_masked_input_and_mask(
1283 x,
1284 org_vocab_start_index=0,
1285 org_vocab_end_index=8,
1286 added_vocab_start_index=8,
1287 added_vocab_end_index=12,
1288 num_org_vocab_padding=2,
1289 )
1290 assert torch.equal(
1291 modified_x, torch.tensor([0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13])
1292 )
1293
1294 # tp 2 case, with padding
1295 modified_x_rank_0, _ = get_masked_input_and_mask(
1296 x,
1297 org_vocab_start_index=0,
1298 org_vocab_end_index=4,
1299 added_vocab_start_index=8,
1300 added_vocab_end_index=10,
1301 num_org_vocab_padding=2,
1302 )
1303 modified_x_rank_1, _ = get_masked_input_and_mask(
1304 x,
1305 org_vocab_start_index=4,
1306 org_vocab_end_index=8,
1307 added_vocab_start_index=10,
1308 added_vocab_end_index=12,
1309 num_org_vocab_padding=2,
1310 )
1311 assert torch.equal(
1312 modified_x_rank_0, torch.tensor([0, 1, 2, 3, 0, 0, 0, 0, 6, 7, 0, 0])
1313 )
1314 assert torch.equal(
1315 modified_x_rank_1, torch.tensor([0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 6, 7])
1316 )
1317
1318 # tp 4 case, with padding
1319 modified_x_rank_0, _ = get_masked_input_and_mask(
1320 x,
1321 org_vocab_start_index=0,
1322 org_vocab_end_index=2,
1323 added_vocab_start_index=8,
1324 added_vocab_end_index=9,
1325 num_org_vocab_padding=2,
1326 )
1327 modified_x_rank_1, _ = get_masked_input_and_mask(
1328 x,
1329 org_vocab_start_index=2,
1330 org_vocab_end_index=4,
1331 added_vocab_start_index=9,
1332 added_vocab_end_index=10,
1333 num_org_vocab_padding=2,
1334 )
1335 modified_x_rank_2, _ = get_masked_input_and_mask(
1336 x,
1337 org_vocab_start_index=4,
1338 org_vocab_end_index=6,
1339 added_vocab_start_index=10,
1340 added_vocab_end_index=11,
1341 num_org_vocab_padding=2,
1342 )
1343 modified_x_rank_3, _ = get_masked_input_and_mask(
1344 x,
1345 org_vocab_start_index=6,
1346 org_vocab_end_index=8,
1347 added_vocab_start_index=11,
1348 added_vocab_end_index=12,
1349 num_org_vocab_padding=2,
1350 )
1351 assert torch.equal(
1352 modified_x_rank_0, torch.tensor([0, 1, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0])
1353 )
1354 assert torch.equal(
1355 modified_x_rank_1, torch.tensor([0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0])
1356 )
1357 assert torch.equal(
1358 modified_x_rank_2, torch.tensor([0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 4, 0])
1359 )
1360 assert torch.equal(
1361 modified_x_rank_3, torch.tensor([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 4])
1362 )
1363
1364
1365 def test_variable_slice_lora_class_selection(default_vllm_config, dist_init):
1366 """Test that MergedColumnParallelLinearVariableSliceWithLoRA is selected
1367 only for nemotron-h style models (checkpoint has single weight but layer
1368 has 3+ output slices).
1369
1370 This verifies that from_layer selects
1371 MergedColumnParallelLinearVariableSliceWithLoRA
1372 before ColumnParallelLinearWithLoRA for layers with 3+ output sizes, since
1373 ColumnParallelLinearWithLoRA's slice_lora_b assumes exactly 2 slices.
1374 """
1375 from vllm.lora.utils import from_layer
1376
1377 lora_config = LoRAConfig(max_loras=8, max_lora_rank=8, lora_dtype=torch.float16)
1378
1379 # Case 1: MergedColumnParallelLinear with 3+ output sizes and
1380 # packed_modules_list with 1 item (nemotron-h style)
1381 # -> MergedColumnParallelLinearVariableSliceWithLoRA should be selected
1382 layer_3_slices = MergedColumnParallelLinear(
1383 4096, [1024, 1280, 1536], bias=False, params_dtype=torch.float16
1384 )
1385 packed_modules_single = ["mlp"]
1386
1387 assert MergedColumnParallelLinearVariableSliceWithLoRA.can_replace_layer(
1388 source_layer=layer_3_slices,
1389 lora_config=lora_config,
1390 packed_modules_list=packed_modules_single,
1391 ), "MergedColumnParallelLinearVariableSliceWithLoRA should handle 3+ slices"
1392
1393 # ColumnParallelLinearWithLoRA should NOT match 3+ slices
1394 # (its slice_lora_b assumes exactly 2 slices)
1395 assert not ColumnParallelLinearWithLoRA.can_replace_layer(
1396 source_layer=layer_3_slices,
1397 lora_config=lora_config,
1398 packed_modules_list=packed_modules_single,
1399 ), (
1400 "ColumnParallelLinearWithLoRA should NOT handle 3+ slices "
1401 "(slice_lora_b assumes 2 slices)"
1402 )
1403
1404 # Verify from_layer selects the correct class (Variable, not base)
1405 selected_layer = from_layer(
1406 layer_3_slices,
1407 max_loras=8,
1408 lora_config=lora_config,
1409 packed_modules_list=packed_modules_single,
1410 )
1411 assert isinstance(
1412 selected_layer, MergedColumnParallelLinearVariableSliceWithLoRA
1413 ), (
1414 f"from_layer should select MergedColumnParallelLinearVariableSliceWithLoRA "
1415 f"for 3+ slices, got {type(selected_layer).__name__}"
1416 )
1417
1418 # Case 2: MergedColumnParallelLinear with 2 output sizes and
1419 # packed_modules_list with 1 item (standard gate_up style)
1420 # -> ColumnParallelLinearWithLoRA should be selected
1421 # -> MergedColumnParallelLinearVariableSliceWithLoRA should NOT match
1422 layer_2_slices = MergedColumnParallelLinear(
1423 4096, [2048, 2048], bias=False, params_dtype=torch.float16
1424 )
1425
1426 assert ColumnParallelLinearWithLoRA.can_replace_layer(
1427 source_layer=layer_2_slices,
1428 lora_config=lora_config,
1429 packed_modules_list=packed_modules_single,
1430 ), "ColumnParallelLinearWithLoRA should handle 2 slices"
1431
1432 assert not MergedColumnParallelLinearVariableSliceWithLoRA.can_replace_layer(
1433 source_layer=layer_2_slices,
1434 lora_config=lora_config,
1435 packed_modules_list=packed_modules_single,
1436 ), "MergedColumnParallelLinearVariableSliceWithLoRA should NOT handle 2 slices"
1437
1438 # Verify from_layer selects ColumnParallelLinearWithLoRA for 2 slices
1439 selected_layer_2 = from_layer(
1440 layer_2_slices,
1441 max_loras=8,
1442 lora_config=lora_config,
1443 packed_modules_list=packed_modules_single,
1444 )
1445 assert isinstance(selected_layer_2, ColumnParallelLinearWithLoRA), (
1446 f"from_layer should select ColumnParallelLinearWithLoRA "
1447 f"for 2 slices, got {type(selected_layer_2).__name__}"
1448 )
1449 # But NOT the Variable subclass
1450 assert not isinstance(
1451 selected_layer_2, MergedColumnParallelLinearVariableSliceWithLoRA
1452 ), (
1453 "from_layer should NOT select "
1454 "MergedColumnParallelLinearVariableSliceWithLoRA for 2 slices"
1455 )
1456
1457 # Case 3: MergedColumnParallelLinear with 3+ items in packed_modules_list
1458 # -> MergedColumnParallelLinearVariableSliceWithLoRA should be selected
1459 packed_modules_three = ["gate_proj", "up_proj", "down_proj"]
1460
1461 assert MergedColumnParallelLinearVariableSliceWithLoRA.can_replace_layer(
1462 source_layer=layer_3_slices,
1463 lora_config=lora_config,
1464 packed_modules_list=packed_modules_three,
1465 ), "MergedColumnParallelLinearVariableSliceWithLoRA should handle 3+ packed modules"
1466
1467 # Case 4: MergedColumnParallelLinear with 2 items in packed_modules_list
1468 # -> MergedColumnParallelLinearWithLoRA should handle this (not Variable)
1469 packed_modules_two = ["gate_proj", "up_proj"]
1470
1471 assert not MergedColumnParallelLinearVariableSliceWithLoRA.can_replace_layer(
1472 source_layer=layer_2_slices,
1473 lora_config=lora_config,
1474 packed_modules_list=packed_modules_two,
1475 ), (
1476 "MergedColumnParallelLinearVariableSliceWithLoRA"
1477 " should NOT handle 2 packed modules"
1478 )
1479
1480 assert MergedColumnParallelLinearWithLoRA.can_replace_layer(
1481 source_layer=layer_2_slices,
1482 lora_config=lora_config,
1483 packed_modules_list=packed_modules_two,
1484 ), "MergedColumnParallelLinearWithLoRA should handle 2 packed modules"
1485
1486 # Verify from_layer selects MergedColumnParallelLinearWithLoRA for 2 packed modules
1487 selected_layer_merged = from_layer(
1488 layer_2_slices,
1489 max_loras=8,
1490 lora_config=lora_config,
1491 packed_modules_list=packed_modules_two,
1492 )
1493 assert isinstance(selected_layer_merged, MergedColumnParallelLinearWithLoRA), (
1494 f"from_layer should select MergedColumnParallelLinearWithLoRA "
1495 f"for 2 packed modules, got {type(selected_layer_merged).__name__}"
1496 )
1497
1498 fully_sharded_tp_lora_config = LoRAConfig(
1499 max_loras=8,
1500 max_lora_rank=16,
1501 lora_dtype=torch.float16,
1502 fully_sharded_loras=True,
1503 )
1504 fully_sharded_tp_layer = MergedColumnParallelLinear(
1505 4096, [2048, 2048], bias=False, params_dtype=torch.float16
1506 )
1507 fully_sharded_tp_layer.tp_size = 2
1508
1509 assert not MergedColumnParallelLinearWithLoRA.can_replace_layer(
1510 source_layer=fully_sharded_tp_layer,
1511 lora_config=fully_sharded_tp_lora_config,
1512 packed_modules_list=packed_modules_two,
1513 ), "Generic merged wrapper should reject fully sharded TP layers"
1514
1515 assert MergedColumnParallelLinearWithShardedLoRA.can_replace_layer(
1516 source_layer=fully_sharded_tp_layer,
1517 lora_config=fully_sharded_tp_lora_config,
1518 packed_modules_list=packed_modules_two,
1519 ), "Sharded merged wrapper should remain eligible for fully sharded TP layers"
1520
1521 selected_fully_sharded_tp_layer = from_layer(
1522 fully_sharded_tp_layer,
1523 max_loras=8,
1524 lora_config=fully_sharded_tp_lora_config,
1525 packed_modules_list=packed_modules_two,
1526 )
1527 assert isinstance(
1528 selected_fully_sharded_tp_layer,
1529 MergedColumnParallelLinearWithShardedLoRA,
1530 ), (
1531 "from_layer should select MergedColumnParallelLinearWithShardedLoRA "
1532 "for fully sharded TP merged layers, got "
1533 f"{type(selected_fully_sharded_tp_layer).__name__}"
1534 )
1535
1536 # Case 5: DeepSeek's fused_qkv_a_proj should reuse the generic merged
1537 # wrapper while preserving its custom base forward path.
1538 deepseek_fused_layer = DeepSeekV2FusedQkvAProjLinear(
1539 4096, [2048, 2048], prefix="model.layers.0.self_attn.fused_qkv_a_proj"
1540 )
1541 selected_deepseek_layer = from_layer(
1542 deepseek_fused_layer,
1543 max_loras=8,
1544 lora_config=lora_config,
1545 packed_modules_list=packed_modules_two,
1546 )
1547 assert isinstance(selected_deepseek_layer, MergedColumnParallelLinearWithLoRA), (
1548 "from_layer should select MergedColumnParallelLinearWithLoRA "
1549 f"for DeepSeek fused_qkv_a_proj, got {type(selected_deepseek_layer).__name__}"
1550 )
1551
1552 fully_sharded_lora_config = LoRAConfig(
1553 max_loras=8,
1554 max_lora_rank=16,
1555 lora_dtype=torch.float16,
1556 fully_sharded_loras=True,
1557 )
1558 selected_fully_sharded_deepseek_layer = from_layer(
1559 deepseek_fused_layer,
1560 max_loras=8,
1561 lora_config=fully_sharded_lora_config,
1562 packed_modules_list=packed_modules_two,
1563 )
1564 assert isinstance(
1565 selected_fully_sharded_deepseek_layer,
1566 MergedColumnParallelLinearWithLoRA,
1567 ), (
1568 "from_layer should keep using MergedColumnParallelLinearWithLoRA "
1569 "for fused_qkv_a_proj when the base layer is effectively unsharded, got "
1570 f"{type(selected_fully_sharded_deepseek_layer).__name__}"
1571 )
1572
1573 # Case 6: Generic subclass of MergedColumnParallelLinear with 2 packed
1574 # modules should still use the generic merged wrapper.
1575 class CustomMergedColumnParallelLinear(MergedColumnParallelLinear):
1576 pass
1577
1578 custom_merged_layer = CustomMergedColumnParallelLinear(
1579 4096, [2048, 2048], bias=False, params_dtype=torch.float16
1580 )
1581 assert MergedColumnParallelLinearWithLoRA.can_replace_layer(
1582 source_layer=custom_merged_layer,
1583 lora_config=lora_config,
1584 packed_modules_list=packed_modules_two,
1585 ), "MergedColumnParallelLinearWithLoRA should handle subclasses"
1586
1587 selected_custom_layer = from_layer(
1588 custom_merged_layer,
1589 max_loras=8,
1590 lora_config=lora_config,
1591 packed_modules_list=packed_modules_two,
1592 )
1593 assert isinstance(selected_custom_layer, MergedColumnParallelLinearWithLoRA), (
1594 f"from_layer should select MergedColumnParallelLinearWithLoRA "
1595 f"for subclassed merged layers, got {type(selected_custom_layer).__name__}"
1596 )
1597
1598 # Case 7: Plain ColumnParallelLinear (not merged) - common in many models
1599 # -> ColumnParallelLinearWithLoRA should be selected
1600 plain_column_parallel = ColumnParallelLinear(
1601 4096, 4096, bias=False, params_dtype=torch.float16
1602 )
1603
1604 assert ColumnParallelLinearWithLoRA.can_replace_layer(
1605 source_layer=plain_column_parallel,
1606 lora_config=lora_config,
1607 packed_modules_list=packed_modules_single,
1608 ), "ColumnParallelLinearWithLoRA should handle plain ColumnParallelLinear"
1609
1610 assert not MergedColumnParallelLinearVariableSliceWithLoRA.can_replace_layer(
1611 source_layer=plain_column_parallel,
1612 lora_config=lora_config,
1613 packed_modules_list=packed_modules_single,
1614 ), (
1615 "MergedColumnParallelLinearVariableSliceWithLoRA "
1616 "should NOT handle plain ColumnParallelLinear"
1617 )
1618
1619 # Verify from_layer selects ColumnParallelLinearWithLoRA for plain layer
1620 selected_plain = from_layer(
1621 plain_column_parallel,
1622 max_loras=8,
1623 lora_config=lora_config,
1624 packed_modules_list=packed_modules_single,
1625 )
1626 assert isinstance(selected_plain, ColumnParallelLinearWithLoRA), (
1627 f"from_layer should select ColumnParallelLinearWithLoRA "
1628 f"for plain ColumnParallelLinear, got {type(selected_plain).__name__}"
1629 )
1630
1631 # Case 8: MergedColumnParallelLinear with exactly 2 output sizes
1632 # and empty packed_modules_list
1633 # -> ColumnParallelLinearWithLoRA should NOT match (packed_modules_list != 1)
1634 # -> MergedColumnParallelLinearVariableSliceWithLoRA should NOT match (< 3 slices)
1635 assert not ColumnParallelLinearWithLoRA.can_replace_layer(
1636 source_layer=layer_2_slices,
1637 lora_config=lora_config,
1638 packed_modules_list=[],
1639 ), "ColumnParallelLinearWithLoRA should NOT handle empty packed_modules_list"
1640
1641 assert not MergedColumnParallelLinearVariableSliceWithLoRA.can_replace_layer(
1642 source_layer=layer_2_slices,
1643 lora_config=lora_config,
1644 packed_modules_list=[],
1645 ), (
1646 "MergedColumnParallelLinearVariableSliceWithLoRA "
1647 "should NOT handle 2 slices even with empty packed_modules_list"
1648 )
1649
1650
1651 @pytest.mark.parametrize(
1652 "wrapper_cls",
1653 [ColumnParallelLinearWithLoRA, ColumnParallelLinearWithShardedLoRA],
1654 )
1655 def test_get_and_maybe_dequant_weights_accepts_lora_wrappers(dist_init, wrapper_cls):
1656 from vllm.model_executor.layers.quantization.utils.quant_utils import (
1657 get_and_maybe_dequant_weights,
1658 )
1659
1660 linear = ColumnParallelLinear(4096, 4096, bias=False, params_dtype=torch.float16)
1661 lora_linear = wrapper_cls(linear)
1662
1663 # Should work with LoRA wrappers and return [out, in] weights.
1664 dequant_weight = get_and_maybe_dequant_weights(lora_linear, out_dtype=torch.float16)
1665 assert dequant_weight.shape == linear.weight.shape
1666
1667
1668 @torch.inference_mode()
1669 @pytest.mark.parametrize("device", DEVICES)
1670 @pytest.mark.parametrize("stage", STAGES)
1671 @pytest.mark.parametrize("fully_sharded", [False, True])
1672 def test_deepseek_fused_qkv_a_proj_lora_preserves_base_forward(
1673 default_vllm_config, dist_init, device, stage, fully_sharded
1674 ):
1675 if current_platform.is_cuda_alike() or current_platform.is_xpu():
1676 torch.accelerator.set_device_index(device)
1677
1678 torch.set_default_device(device)
1679 dtype = (
1680 torch.float16
1681 if (current_platform.is_cuda_alike() or current_platform.is_xpu())
1682 else torch.float32
1683 )
1684 max_loras = 8
1685 lora_config = LoRAConfig(
1686 max_loras=max_loras,
1687 max_lora_rank=8,
1688 lora_dtype=dtype,
1689 fully_sharded_loras=fully_sharded,
1690 )
1691 punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
1692 assert check_punica_wrapper(punica_wrapper)
1693
1694 class OffsetDeepSeekFusedQkvAProjLinear(DeepSeekV2FusedQkvAProjLinear):
1695 def forward(self, input_):
1696 output, output_bias = super().forward(input_)
1697 return output + 1, output_bias
1698
1699 layer = OffsetDeepSeekFusedQkvAProjLinear(
1700 32, [16, 16], prefix="model.layers.0.self_attn.fused_qkv_a_proj"
1701 )
1702 layer.weight.data = torch.rand_like(layer.weight.data, dtype=dtype)
1703
1704 lora_layer = MergedColumnParallelLinearWithLoRA(layer)
1705 lora_layer.create_lora_weights(max_loras, lora_config)
1706 lora_layer.set_mapping(punica_wrapper)
1707
1708 id_to_index = get_random_id_to_index(1, max_loras, log=False)
1709 active_slot = next(i for i, lora_id in enumerate(id_to_index) if lora_id == 1)
1710 lora_a = [
1711 torch.rand(8, 32, dtype=dtype, device=device),
1712 torch.rand(8, 32, dtype=dtype, device=device),
1713 ]
1714 lora_b = [
1715 torch.rand(16, 8, dtype=dtype, device=device),
1716 torch.rand(16, 8, dtype=dtype, device=device),
1717 ]
1718 lora_layer.set_lora(active_slot, lora_a=lora_a, lora_b=lora_b)
1719
1720 inputs, index_mapping, prompt_mapping = create_random_inputs(
1721 active_lora_ids=[1],
1722 num_inputs=4,
1723 input_size=(1, 32),
1724 input_range=(0, 1),
1725 input_type=dtype,
1726 device=device,
1727 )
1728 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
1729 punica_wrapper.update_metadata(lora_mapping, id_to_index, max_loras, 512)
1730
1731 lora_result = lora_layer(torch.cat(inputs))[0]
1732
1733 expected_results = []
1734 for input_ in inputs:
1735 result = layer(input_)[0]
1736 result[:, :16] += input_ @ lora_a[0].T @ lora_b[0].T
1737 result[:, 16:] += input_ @ lora_a[1].T @ lora_b[1].T
1738 expected_results.append(result)
1739
1740 rtol, atol = TOLERANCES[lora_result.dtype]
1741 torch.testing.assert_close(
1742 lora_result, torch.cat(expected_results), rtol=rtol, atol=atol
1743 )
1744
1745 merged_layer = OffsetDeepSeekFusedQkvAProjLinear(
1746 32, [16, 16], prefix="model.layers.0.self_attn.fused_qkv_a_proj"
1747 )
1748 merged_layer.weight.data = layer.weight.data.clone()
1749 merged_layer.weight.data[:16].add_(lora_b[0] @ lora_a[0])
1750 merged_layer.weight.data[16:].add_(lora_b[1] @ lora_a[1])
1751 merged_result = merged_layer(torch.cat(inputs))[0]
1752
1753 torch.testing.assert_close(lora_result, merged_result, rtol=rtol, atol=atol)
1754
1755
1756 @torch.inference_mode()
1757 @pytest.mark.parametrize("device", DEVICES)
1758 @pytest.mark.parametrize("stage", STAGES)
1759 def test_replicated_lora_preserves_base_forward_for_subclasses(
1760 default_vllm_config, dist_init, device, stage
1761 ):
1762 if current_platform.is_cuda_alike() or current_platform.is_xpu():
1763 torch.accelerator.set_device_index(device)
1764
1765 torch.set_default_device(device)
1766 dtype = (
1767 torch.float16
1768 if current_platform.is_cuda_alike() or current_platform.is_xpu()
1769 else torch.float32
1770 )
1771 max_loras = 8
1772 lora_config = LoRAConfig(max_loras=max_loras, max_lora_rank=8, lora_dtype=dtype)
1773 punica_wrapper = get_punica_wrapper(8192, 256, device, lora_config=lora_config)
1774 assert check_punica_wrapper(punica_wrapper)
1775
1776 class OffsetReplicatedLinear(ReplicatedLinear):
1777 def forward(self, input_):
1778 output, output_bias = super().forward(input_)
1779 return output + 1, output_bias
1780
1781 layer = OffsetReplicatedLinear(32, 16, bias=False, params_dtype=dtype)
1782 layer.weight.data = torch.rand_like(layer.weight.data, dtype=dtype)
1783
1784 lora_layer = ReplicatedLinearWithLoRA(layer)
1785 lora_layer.create_lora_weights(max_loras, lora_config)
1786 lora_layer.set_mapping(punica_wrapper)
1787
1788 id_to_index = get_random_id_to_index(1, max_loras, log=False)
1789 active_slot = next(i for i, lora_id in enumerate(id_to_index) if lora_id == 1)
1790 lora_a = torch.rand(8, 32, dtype=dtype, device=device)
1791 lora_b = torch.rand(16, 8, dtype=dtype, device=device)
1792 lora_layer.set_lora(active_slot, lora_a=lora_a, lora_b=lora_b)
1793
1794 inputs, index_mapping, prompt_mapping = create_random_inputs(
1795 active_lora_ids=[1],
1796 num_inputs=4,
1797 input_size=(1, 32),
1798 input_range=(0, 1),
1799 input_type=dtype,
1800 device=device,
1801 )
1802 lora_mapping = LoRAMapping(index_mapping, prompt_mapping, is_prefill=stage)
1803 punica_wrapper.update_metadata(lora_mapping, id_to_index, max_loras, 512)
1804
1805 lora_result = lora_layer(torch.cat(inputs))[0]
1806
1807 expected_results = []
1808 for input_ in inputs:
1809 result = layer(input_)[0]
1810 result += input_ @ lora_a.T @ lora_b.T
1811 expected_results.append(result)
1812
1813 rtol, atol = TOLERANCES[lora_result.dtype]
1814 torch.testing.assert_close(
1815 lora_result, torch.cat(expected_results), rtol=rtol, atol=atol
1816 )
1817
1818 merged_layer = OffsetReplicatedLinear(32, 16, bias=False, params_dtype=dtype)
1819 merged_layer.weight.data = layer.weight.data.clone()
1820 merged_layer.weight.data.add_(lora_b @ lora_a)
1821 merged_result = merged_layer(torch.cat(inputs))[0]
1822
1823 torch.testing.assert_close(lora_result, merged_result, rtol=rtol, atol=atol)