| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | import json |
| 5 | import math |
| 6 | import shutil |
| 7 | |
| 8 | import pytest |
| 9 | |
| 10 | from vllm.config.lora import LoRAConfig |
| 11 | from vllm.lora.peft_helper import PEFTHelper |
| 12 | |
| 13 | ERROR_CASES = [ |
| 14 | ( |
| 15 | "test_rank", |
| 16 | {"r": 1024}, |
| 17 | "is greater than max_lora_rank", |
| 18 | ), |
| 19 | ("test_dora", {"use_dora": True}, "does not yet support DoRA"), |
| 20 | ( |
| 21 | "test_modules_to_save", |
| 22 | {"modules_to_save": ["lm_head"]}, |
| 23 | "Unsupported modules_to_save", |
| 24 | ), |
| 25 | ("test_rank_zero", {"r": 0}, "must be a positive integer"), |
| 26 | ("test_rank_negative", {"r": -8}, "must be a positive integer"), |
| 27 | ] |
| 28 | |
| 29 | |
| 30 | def test_peft_helper_pass(llama32_lora_files, tmp_path): |
| 31 | peft_helper = PEFTHelper.from_local_dir( |
| 32 | llama32_lora_files, max_position_embeddings=4096 |
| 33 | ) |
| 34 | lora_config = LoRAConfig(max_lora_rank=16, max_cpu_loras=3, max_loras=2) |
| 35 | peft_helper.validate_legal(lora_config) |
| 36 | assert peft_helper.r == 8 |
| 37 | assert peft_helper.lora_alpha == 32 |
| 38 | target_modules = sorted(peft_helper.target_modules) |
| 39 | |
| 40 | assert target_modules == [ |
| 41 | "down_proj", |
| 42 | "embed_tokens", |
| 43 | "gate_proj", |
| 44 | "k_proj", |
| 45 | "lm_head", |
| 46 | "o_proj", |
| 47 | "q_proj", |
| 48 | "up_proj", |
| 49 | "v_proj", |
| 50 | ] |
| 51 | assert peft_helper.vllm_max_position_embeddings == 4096 |
| 52 | |
| 53 | # test RSLoRA |
| 54 | rslora_config = dict(use_rslora=True) |
| 55 | test_dir = tmp_path / "test_rslora" |
| 56 | shutil.copytree(llama32_lora_files, test_dir) |
| 57 | |
| 58 | # Load and modify configuration |
| 59 | config_path = test_dir / "adapter_config.json" |
| 60 | with open(config_path) as f: |
| 61 | adapter_config = json.load(f) |
| 62 | # Apply configuration changes |
| 63 | adapter_config.update(rslora_config) |
| 64 | |
| 65 | # Save modified configuration |
| 66 | with open(config_path, "w") as f: |
| 67 | json.dump(adapter_config, f) |
| 68 | |
| 69 | peft_helper = PEFTHelper.from_local_dir(test_dir, max_position_embeddings=4096) |
| 70 | peft_helper.validate_legal(lora_config) |
| 71 | scaling = peft_helper.lora_alpha / math.sqrt(peft_helper.r) |
| 72 | assert abs(peft_helper.vllm_lora_scaling_factor - scaling) < 1e-3 |
| 73 | |
| 74 | |
| 75 | @pytest.mark.parametrize("test_name,config_change,expected_error", ERROR_CASES) |
| 76 | def test_peft_helper_error( |
| 77 | llama32_lora_files, |
| 78 | tmp_path, |
| 79 | test_name: str, |
| 80 | config_change: dict, |
| 81 | expected_error: str, |
| 82 | ): |
| 83 | test_dir = tmp_path / test_name |
| 84 | shutil.copytree(llama32_lora_files, test_dir) |
| 85 | |
| 86 | # Load and modify configuration |
| 87 | config_path = test_dir / "adapter_config.json" |
| 88 | with open(config_path) as f: |
| 89 | adapter_config = json.load(f) |
| 90 | # Apply configuration changes |
| 91 | adapter_config.update(config_change) |
| 92 | |
| 93 | # Save modified configuration |
| 94 | with open(config_path, "w") as f: |
| 95 | json.dump(adapter_config, f) |
| 96 | lora_config = LoRAConfig(max_lora_rank=16, max_cpu_loras=3, max_loras=2) |
| 97 | # Test loading the adapter |
| 98 | with pytest.raises(ValueError, match=expected_error): |
| 99 | PEFTHelper.from_local_dir( |
| 100 | test_dir, max_position_embeddings=4096 |
| 101 | ).validate_legal(lora_config) |
| 102 | |
| 103 | |
| 104 | @pytest.mark.parametrize("bad_rank", [0, -1, -8]) |
| 105 | def test_peft_helper_invalid_rank_direct(bad_rank: int): |
| 106 | """Regression test: constructing a PEFTHelper with a non-positive rank |
| 107 | must raise a clear ValueError instead of crashing with an unrelated |
| 108 | ZeroDivisionError (r=0) or silently succeeding with a sign-flipped |
| 109 | scaling factor that validate_legal() never catches (r<0, since its only |
| 110 | rank check is the upper bound against max_lora_rank). |
| 111 | |
| 112 | Network-free: constructs PEFTHelper directly rather than going through |
| 113 | from_local_dir(), which needs an on-disk adapter_config.json. |
| 114 | """ |
| 115 | with pytest.raises(ValueError, match="must be a positive integer"): |
| 116 | PEFTHelper(r=bad_rank, lora_alpha=16, target_modules=["q_proj"]) |