mirror of https://github.com/razor-ai/soup.git
99 lines
3.8 KiB
Python
99 lines
3.8 KiB
Python
"""Tests for v0.39.0 Part C — per-pattern LoRA rank/alpha."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from soup_cli.config.schema import LoraConfig
|
|
from soup_cli.utils.peft_builder import build_peft_config
|
|
|
|
|
|
class TestRankPatternSchema:
|
|
def test_default_none(self):
|
|
cfg = LoraConfig()
|
|
assert cfg.rank_pattern is None
|
|
assert cfg.alpha_pattern is None
|
|
|
|
def test_rank_pattern_dict_accepted(self):
|
|
cfg = LoraConfig(rank_pattern={"q_proj": 8, "v_proj": 16})
|
|
assert cfg.rank_pattern == {"q_proj": 8, "v_proj": 16}
|
|
|
|
def test_alpha_pattern_dict_accepted(self):
|
|
cfg = LoraConfig(alpha_pattern={"q_proj": 16, "v_proj": 32})
|
|
assert cfg.alpha_pattern == {"q_proj": 16, "v_proj": 32}
|
|
|
|
def test_rank_pattern_rejects_non_int_value(self):
|
|
with pytest.raises(ValidationError):
|
|
LoraConfig(rank_pattern={"q_proj": "high"})
|
|
|
|
def test_rank_pattern_rejects_negative(self):
|
|
with pytest.raises(ValidationError):
|
|
LoraConfig(rank_pattern={"q_proj": -1})
|
|
|
|
def test_rank_pattern_rejects_zero(self):
|
|
with pytest.raises(ValidationError):
|
|
LoraConfig(rank_pattern={"q_proj": 0})
|
|
|
|
def test_rank_pattern_rejects_too_large(self):
|
|
with pytest.raises(ValidationError):
|
|
LoraConfig(rank_pattern={"q_proj": 10_000})
|
|
|
|
def test_rank_pattern_rejects_empty_key(self):
|
|
with pytest.raises(ValidationError):
|
|
LoraConfig(rank_pattern={"": 8})
|
|
|
|
def test_rank_pattern_rejects_null_byte_key(self):
|
|
with pytest.raises(ValidationError):
|
|
LoraConfig(rank_pattern={"q\x00proj": 8})
|
|
|
|
def test_rank_pattern_rejects_too_many_keys(self):
|
|
# Cap at 256 patterns to prevent absurd configs
|
|
big = {f"k{i}": 8 for i in range(257)}
|
|
with pytest.raises(ValidationError):
|
|
LoraConfig(rank_pattern=big)
|
|
|
|
def test_rank_pattern_rejects_bool_value(self):
|
|
# bool is subclass of int in Python — exclude explicitly
|
|
with pytest.raises(ValidationError):
|
|
LoraConfig(rank_pattern={"q_proj": True})
|
|
|
|
|
|
class TestRankPatternMutualExclusion:
|
|
def test_rank_pattern_with_vera_rejected(self):
|
|
with pytest.raises(ValidationError, match="rank_pattern"):
|
|
LoraConfig(use_vera=True, rank_pattern={"q_proj": 8})
|
|
|
|
def test_alpha_pattern_with_vera_rejected(self):
|
|
with pytest.raises(ValidationError, match="alpha_pattern"):
|
|
LoraConfig(use_vera=True, alpha_pattern={"q_proj": 16})
|
|
|
|
def test_rank_pattern_with_dora_ok(self):
|
|
# DoRA still uses standard LoraConfig; rank_pattern works
|
|
cfg = LoraConfig(use_dora=True, rank_pattern={"q_proj": 8})
|
|
assert cfg.rank_pattern == {"q_proj": 8}
|
|
|
|
|
|
class TestPeftBuilderRankPattern:
|
|
def test_rank_pattern_propagated(self):
|
|
cfg = LoraConfig(rank_pattern={"q_proj": 8, "v_proj": 16})
|
|
spec = build_peft_config(cfg, target_modules="auto", task_type="CAUSAL_LM")
|
|
assert spec["init_kwargs"]["rank_pattern"] == {"q_proj": 8, "v_proj": 16}
|
|
|
|
def test_alpha_pattern_propagated(self):
|
|
cfg = LoraConfig(alpha_pattern={"q_proj": 16})
|
|
spec = build_peft_config(cfg, target_modules="auto", task_type="CAUSAL_LM")
|
|
assert spec["init_kwargs"]["alpha_pattern"] == {"q_proj": 16}
|
|
|
|
def test_neither_pattern_omitted_when_none(self):
|
|
cfg = LoraConfig()
|
|
spec = build_peft_config(cfg, target_modules="auto", task_type="CAUSAL_LM")
|
|
assert "rank_pattern" not in spec["init_kwargs"]
|
|
assert "alpha_pattern" not in spec["init_kwargs"]
|
|
|
|
def test_vera_path_ignores_patterns_when_unset(self):
|
|
cfg = LoraConfig(use_vera=True)
|
|
spec = build_peft_config(cfg, target_modules="auto", task_type="CAUSAL_LM")
|
|
assert spec["peft_cls"] == "VeraConfig"
|
|
assert "rank_pattern" not in spec["init_kwargs"]
|