Trim and tighten code comments and docstrings across the repository. Comment-only: every changed file verified code-identical to main via AST/token comparison.
361 lines
13 KiB
Python
361 lines
13 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team.
|
|
"""Pinned-symbol compat check across PEFT PyPI minor versions
|
|
unsloth + unsloth-zoo target. Catches API drift like:
|
|
|
|
- peft 0.18 finalised the LoraConfig public surface (+ MoE-aware
|
|
target_modules); unsloth uses target_modules + r + lora_alpha +
|
|
lora_dropout + bias.
|
|
- peft 0.19 introduced the LoraConfig.target_parameters extension;
|
|
unsloth-zoo's MoE LoRA extractor in saving_utils.py reads it via
|
|
getattr() so missing on older versions is OK but the attribute
|
|
shape must remain stable on >= 0.19.
|
|
- peft.tuners.lora package layout: LoraLayer / LoraConfig / Linear4bit
|
|
re-exports must keep working under both `from peft import X` and
|
|
`from peft.tuners.lora import X`.
|
|
|
|
Strategy: for each tracked PEFT tag, fetch source from
|
|
github.com/huggingface/peft (no pip install needed) and assert that
|
|
every symbol unsloth + unsloth-zoo's PEFT touchpoints depend on is
|
|
present.
|
|
|
|
Versioning policy: cover the supported window declared in
|
|
unsloth/pyproject.toml (`peft>=0.18.0,!=0.11.0`) plus `main`. The
|
|
`!=0.11.0` exclusion is for the historical broken release; we don't
|
|
test against it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from tests.version_compat._fetch import fetch_text, first_match, has_def
|
|
|
|
|
|
# pyproject pin: peft>=0.18.0. Test the floor + each minor since.
|
|
# `main` catches breakage before a release lands.
|
|
PEFT_TAGS = [
|
|
"v0.18.0",
|
|
"v0.18.1",
|
|
"v0.19.0",
|
|
"v0.19.1",
|
|
"main",
|
|
]
|
|
|
|
|
|
# Top-level public re-exports. unsloth/models/sentence_transformer.py:1948
|
|
# does `from peft import LoraConfig, get_peft_model as peft_get_peft_model`.
|
|
# unsloth_zoo's saving_utils + lora extractors hit `peft.PeftModel`.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_top_level_exports(tag: str):
|
|
src = fetch_text("huggingface/peft", tag, "src/peft/__init__.py")
|
|
assert src is not None, f"{tag}: src/peft/__init__.py missing"
|
|
needed = (
|
|
"LoraConfig",
|
|
"get_peft_model",
|
|
"PeftModel",
|
|
)
|
|
missing = [n for n in needed if n not in src]
|
|
assert not missing, (
|
|
f"{tag}: peft top-level missing {missing}; "
|
|
f"unsloth.models.sentence_transformer:1948 + unsloth-zoo saving_utils "
|
|
f"will ImportError"
|
|
)
|
|
|
|
|
|
# LoraConfig at the canonical sub-module path: peft.tuners.lora.LoraConfig
|
|
# (or peft.tuners.lora.config.LoraConfig). unsloth-zoo's normaliser inspects
|
|
# it via getattr() and dataclass field introspection.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_lora_config_class(tag: str):
|
|
candidates = [
|
|
"src/peft/tuners/lora/config.py",
|
|
"src/peft/tuners/lora/__init__.py",
|
|
"src/peft/tuners/lora.py",
|
|
]
|
|
found_in = []
|
|
for p in candidates:
|
|
src = fetch_text("huggingface/peft", tag, p)
|
|
if src is not None and has_def(src, "LoraConfig", "class"):
|
|
found_in.append(p)
|
|
assert found_in, f"{tag}: peft.tuners.lora.LoraConfig not in any of {candidates}"
|
|
|
|
|
|
# get_peft_model: top-level helper used by sentence_transformer.py:2043.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_get_peft_model_function(tag: str):
|
|
"""`def get_peft_model(...)` may live in mapping.py (older
|
|
layout) or mapping_func.py (peft 0.18+ split). Either is fine."""
|
|
candidates = [
|
|
"src/peft/mapping.py",
|
|
"src/peft/mapping_func.py",
|
|
"src/peft/__init__.py",
|
|
"src/peft/peft_model.py",
|
|
]
|
|
for p in candidates:
|
|
src = fetch_text("huggingface/peft", tag, p)
|
|
if src is not None and has_def(src, "get_peft_model", "func"):
|
|
return
|
|
pytest.fail(f"{tag}: def get_peft_model(...) not found in any of {candidates}")
|
|
|
|
|
|
# LoraLayer base class: unsloth-zoo's MoE LoRA extractor walks subclasses of
|
|
# peft.tuners.lora.LoraLayer. A rename/move makes the walk silently return 0.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_lora_layer_class(tag: str):
|
|
candidates = [
|
|
"src/peft/tuners/lora/layer.py",
|
|
"src/peft/tuners/lora/__init__.py",
|
|
"src/peft/tuners/lora.py",
|
|
]
|
|
for p in candidates:
|
|
src = fetch_text("huggingface/peft", tag, p)
|
|
if src is not None and has_def(src, "LoraLayer", "class"):
|
|
return
|
|
pytest.fail(
|
|
f"{tag}: class LoraLayer not in any of {candidates} — "
|
|
f"unsloth-zoo MoE LoRA extractor relies on isinstance checks "
|
|
f"against this class"
|
|
)
|
|
|
|
|
|
# bnb-aware LoRA: peft.tuners.lora.bnb is the bitsandbytes integration point.
|
|
# Missing it -> 4bit LoRA silently falls back to fp16 (bigger memory footprint).
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_lora_bnb_integration(tag: str):
|
|
candidates = [
|
|
"src/peft/tuners/lora/bnb.py",
|
|
"src/peft/tuners/lora/_bnb.py",
|
|
]
|
|
for p in candidates:
|
|
src = fetch_text("huggingface/peft", tag, p)
|
|
if src is None:
|
|
continue
|
|
# The Linear4bit subclass naming is the contract -- either name
|
|
# is fine, but at least one bnb-flavoured Linear must exist.
|
|
has_4bit = any(
|
|
cls in src
|
|
for cls in (
|
|
"class Linear4bit",
|
|
"class Linear8bitLt",
|
|
"class _Linear4bit",
|
|
"class _Linear8bitLt",
|
|
)
|
|
)
|
|
if has_4bit:
|
|
return
|
|
pytest.fail(
|
|
f"{tag}: peft.tuners.lora.bnb missing or no Linear4bit/Linear8bitLt "
|
|
f"class found; unsloth's 4-bit LoRA path silently degrades to fp16"
|
|
)
|
|
|
|
|
|
# Coverage extension (added 2026-05): symbols from the 8-PR audit
|
|
# unsloth#5015, #5167, #5036, #4807 + unsloth-zoo#618, #596, #482, #430.
|
|
|
|
|
|
# 1. peft.tuners.lora.layer.VARIANT_KWARG_KEYS — added in peft 0.18.
|
|
# unsloth-zoo#430 injects the import into the compiled forward.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_variant_kwarg_keys_const(tag: str):
|
|
src = fetch_text("huggingface/peft", tag, "src/peft/tuners/lora/layer.py")
|
|
if src is None:
|
|
pytest.skip(f"{tag}: src/peft/tuners/lora/layer.py missing")
|
|
if "VARIANT_KWARG_KEYS" not in src:
|
|
pytest.fail(
|
|
f"{tag}: peft.tuners.lora.layer.VARIANT_KWARG_KEYS missing; "
|
|
f"unsloth_zoo/compiler.py:2645 import injection breaks (unsloth-zoo#430)"
|
|
)
|
|
|
|
|
|
# 2. peft.tuners.lora.layer.ParamWrapper — peft 0.18 added the class for MoE
|
|
# 3D-parameter LoRA. unsloth-zoo#618 monkey-patches the MoE LoRA extractor.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_param_wrapper_class(tag: str):
|
|
src = fetch_text("huggingface/peft", tag, "src/peft/tuners/lora/layer.py")
|
|
if src is None:
|
|
pytest.skip(f"{tag}: layer.py missing")
|
|
assert has_def(src, "ParamWrapper", "class"), (
|
|
f"{tag}: peft.tuners.lora.layer.ParamWrapper missing; "
|
|
f"unsloth_zoo/temporary_patches/qwen3_moe.py:43-130 + "
|
|
f"moe_utils.py:757 ImportError (unsloth-zoo#618)"
|
|
)
|
|
# Member names: informational only; the real bug to catch is full removal.
|
|
for name in ("parameter_name", "forward", "lora_A", "get_base_layer"):
|
|
_present = name in src
|
|
|
|
|
|
# 3. peft.tuners.lora.LoraConfig.target_parameters — peft 0.19+. Used by
|
|
# unsloth-zoo's MoE target-parameter extractor.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_lora_config_target_parameters(tag: str):
|
|
src = fetch_text("huggingface/peft", tag, "src/peft/tuners/lora/config.py")
|
|
if src is None:
|
|
pytest.skip(f"{tag}: src/peft/tuners/lora/config.py missing")
|
|
# Optional on 0.18.x, required from 0.19.0+
|
|
has_it = "target_parameters" in src
|
|
if "0.18" in tag and not has_it:
|
|
pytest.skip(f"{tag}: target_parameters not yet introduced (peft 0.18)")
|
|
assert has_it, (
|
|
f"{tag}: LoraConfig.target_parameters missing on peft >=0.19; "
|
|
f"unsloth-zoo MoE target-parameter extraction breaks"
|
|
)
|
|
|
|
|
|
# 4. peft.tuners.lora.model.LoraModel._create_and_replace — unsloth#4807
|
|
# monkey-patches this for Gemma4ClippableLinear.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_lora_model_create_and_replace(tag: str):
|
|
src = fetch_text("huggingface/peft", tag, "src/peft/tuners/lora/model.py")
|
|
if src is None:
|
|
pytest.skip(f"{tag}: src/peft/tuners/lora/model.py missing")
|
|
assert has_def(src, "LoraModel", "class"), f"{tag}: class LoraModel missing"
|
|
assert has_def(src, "_create_and_replace", "func"), (
|
|
f"{tag}: LoraModel._create_and_replace missing; "
|
|
f"unsloth/models/loader.py:1535-1601 monkey-patch breaks (unsloth#4807)"
|
|
)
|
|
|
|
|
|
# 5. peft.utils.transformers_weight_conversion.build_peft_weight_mapping —
|
|
# unsloth#5167 wraps it to handle WeightConversion.__init__ kwargs.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_transformers_weight_conversion_module(tag: str):
|
|
candidates = [
|
|
"src/peft/utils/transformers_weight_conversion.py",
|
|
"src/peft/utils/transformers_weight_conversion/__init__.py",
|
|
]
|
|
hit = first_match("huggingface/peft", tag, candidates)
|
|
if hit is None:
|
|
pytest.skip(f"{tag}: transformers_weight_conversion not present (legacy peft)")
|
|
_, src = hit
|
|
assert (
|
|
has_def(src, "build_peft_weight_mapping", "func") or "build_peft_weight_mapping" in src
|
|
), (
|
|
f"{tag}: build_peft_weight_mapping missing in transformers_weight_conversion; "
|
|
f"unsloth/import_fixes.py:1375-1456 wrap breaks (unsloth#5167)"
|
|
)
|
|
|
|
|
|
# 6. peft.utils.integrations.dequantize_module_weight — used by 3 unsloth/
|
|
# unsloth-zoo callsites.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_integrations_dequantize_module_weight(tag: str):
|
|
candidates = [
|
|
"src/peft/utils/integrations.py",
|
|
"src/peft/utils/integrations/__init__.py",
|
|
]
|
|
hit = first_match("huggingface/peft", tag, candidates)
|
|
assert hit is not None, f"{tag}: src/peft/utils/integrations[.py|/__init__.py] both missing"
|
|
_, src = hit
|
|
assert has_def(src, "dequantize_module_weight", "func") or "dequantize_module_weight" in src, (
|
|
f"{tag}: peft.utils.integrations.dequantize_module_weight missing; "
|
|
f"unsloth-zoo vllm_utils.py:2701, unsloth/_utils.py:1550, "
|
|
f"saving_utils.py:270 ImportError"
|
|
)
|
|
|
|
|
|
# 7. peft.PeftType.LORA — used by unsloth-zoo vllm_utils.py:2520-2559.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_type_lora_enum(tag: str):
|
|
candidates = [
|
|
"src/peft/utils/peft_types.py",
|
|
"src/peft/utils/__init__.py",
|
|
"src/peft/__init__.py",
|
|
]
|
|
for p in candidates:
|
|
src = fetch_text("huggingface/peft", tag, p)
|
|
if src is None:
|
|
continue
|
|
# Either `class PeftType(...)` definition with LORA member, or
|
|
# re-export from a submodule.
|
|
if "PeftType" in src and ("LORA" in src or "lora" in src.lower()):
|
|
return
|
|
pytest.fail(
|
|
f"{tag}: peft.PeftType (with LORA member) not in any of {candidates}; "
|
|
f"unsloth-zoo vllm_utils.py:2520 reference breaks"
|
|
)
|
|
|
|
|
|
# 8. peft.utils.ModulesToSaveWrapper — both peft.utils.* and
|
|
# peft.utils.other.* import paths used.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_modules_to_save_wrapper(tag: str):
|
|
candidates = [
|
|
"src/peft/utils/other.py",
|
|
"src/peft/utils/__init__.py",
|
|
]
|
|
found_in = []
|
|
for p in candidates:
|
|
src = fetch_text("huggingface/peft", tag, p)
|
|
if src is None:
|
|
continue
|
|
if has_def(src, "ModulesToSaveWrapper", "class"):
|
|
found_in.append(p)
|
|
assert found_in, (
|
|
f"{tag}: ModulesToSaveWrapper not defined in {candidates}; "
|
|
f"unsloth/training_utils.py:239 + models/llama.py:153 ImportError"
|
|
)
|
|
|
|
|
|
# 9. peft.PeftModel.from_pretrained signature pin — unsloth#4807.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_peft_model_from_pretrained_signature(tag: str):
|
|
src = fetch_text("huggingface/peft", tag, "src/peft/peft_model.py")
|
|
assert src is not None, f"{tag}: src/peft/peft_model.py missing"
|
|
# Just check the method name; the full kwarg list is too brittle.
|
|
assert has_def(
|
|
src, "from_pretrained", "func"
|
|
), f"{tag}: PeftModel.from_pretrained missing in peft_model.py"
|
|
|
|
|
|
# 10. peft.__version__ exported via known mechanism.
|
|
|
|
|
|
@pytest.mark.parametrize("tag", PEFT_TAGS)
|
|
def test_peft_version_parseable(tag: str):
|
|
src = fetch_text("huggingface/peft", tag, "src/peft/__init__.py")
|
|
assert src is not None
|
|
# Same gates as the TRL test: literal / submodule / metadata
|
|
has_literal = bool(re.search(r'^__version__\s*=\s*["\']', src, re.MULTILINE))
|
|
has_subimport = bool(re.search(r"^from\s+\.version\s+import\s+__version__", src, re.MULTILINE))
|
|
has_metadata = bool(
|
|
re.search(
|
|
r"^from\s+importlib\.metadata\s+import\s+(?:[\w,\s]+,\s*)?version",
|
|
src,
|
|
re.MULTILINE,
|
|
)
|
|
and re.search(r"^\s*__version__\s*=\s*version\s*\(", src, re.MULTILINE)
|
|
)
|
|
assert (
|
|
has_literal or has_subimport or has_metadata
|
|
), f"{tag}: peft.__version__ not exported via any known mechanism"
|