Skip to content

fix(save_and_load): save base_layer.bias for bias="lora_only"#3307

Open
Anai-Guo wants to merge 1 commit into
huggingface:mainfrom
Anai-Guo:fix/lora-only-base-layer-bias-save
Open

fix(save_and_load): save base_layer.bias for bias="lora_only"#3307
Anai-Guo wants to merge 1 commit into
huggingface:mainfrom
Anai-Guo:fix/lora-only-base-layer-bias-save

Conversation

@Anai-Guo

@Anai-Guo Anai-Guo commented Jun 7, 2026

Copy link
Copy Markdown

What

LoraConfig(bias="lora_only") correctly marks a targeted layer's bias as trainable, but get_peft_model_state_dict() / save_pretrained() silently drop it, so a reloaded adapter does not reproduce the trained outputs.

Fixes #3306

Why

For a targeted layer the original module is wrapped, and its bias lives at <module>.base_layer.bias. The bias-key reconstruction was:

bias_name = k.split("lora_")[0] + "bias"   # -> base_model.model.proj.bias

but the real trained key is base_model.model.proj.base_layer.bias, so the lookup never matched and the bias was excluded from the saved state dict. (bias="all" works because it keys off the substring "bias" directly.)

Fix

Look up <prefix>base_layer.bias (current tuner-layer structure) in addition to the legacy <prefix>bias. The same one-line pattern was applied to the identical bias="boft_only" branch for BOFT.

Test

Added TestLoraInitialization::test_lora_only_bias_is_saved_and_reloaded, which perturbs all trainable params (incl. the base_layer bias), asserts the bias key is present in both get_peft_model_state_dict() output and the saved adapter_model.safetensors, and checks that reloading reproduces the pre-save output.

Reproducer from the issue now round-trips with max diff 0.0 (was 1.21).

🤖 Generated with Claude Code

@githubnemo githubnemo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move the test around a bit, it doesn't make sense to have that in test_initializations.py since this is not about initializations at all. I propose to create a new test in test_custom_models.py similar to how test_disable_adapter_with_bias_warns works by adding the bias attribute. But we can keep the basic idea of your test. If we find more methods that are buggy this way, we don't have to fix those right now, instead we can mark them as xfail.

It would also be good to test that the bias is not in the state dict when the options isn't set. This would fail right now, I think.

With bias="lora_only" (or boft_only) the trained bias of a targeted
module lives under the wrapped base_layer at "<module>.base_layer.bias",
so get_peft_model_state_dict missed it and reloading did not reproduce
the trained outputs. Check both the base_layer and the legacy bias name.

Adds a regression test in test_custom_models.py (next to
test_disable_adapter_with_bias_warns) that perturbs the trained params,
saves/reloads with bias="lora_only" and checks output parity, and also
asserts the base_layer bias is absent with bias="none".

Fixes huggingface#3306
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LoraConfig(bias="lora_only") trains base_layer.bias but save_pretrained/get_peft_model_state_dict drops it

2 participants