LoRA fine-tuning with PEFT

Hugging Face Transformers · Practical · 15 min

LoRA cuts trainable parameters by 100x or more, making LLM fine-tuning feasible on a single GPU.

Full fine-tuning updates every weight and needs memory for gradients and optimizer state for all of them. LoRA (Low-Rank Adaptation) freezes the original weights and learns a small update for selected linear layers: W + (alpha / r) * B @ A, where A and B have a small rank r (8 to 64). Only A and B are trained, usually well under 1% of parameters.

With the PEFT library (pip install peft):

config = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05,
                    target_modules=["q_proj", "v_proj"], task_type="CAUSAL_LM")
model = get_peft_model(model, config)
model.print_trainable_parameters()
  • target_modules names the layers to adapt; module names differ per architecture (q_proj/v_proj in Llama-style models, query/value in BERT). "all-linear" targets every linear layer and often works best.
  • Use a higher learning rate than full fine-tuning (around 1e-4 to 2e-4).
  • The wrapped model works with Trainer unchanged.

model.save_pretrained(dir) saves only the adapter (a few MB). Load it later with PeftModel.from_pretrained(base_model, dir), or call merge_and_unload() to fold the adapter into the base weights for deployment without the PEFT dependency. Several adapters can be loaded onto one base model and switched per request.

Example

from peft import LoraConfig, PeftModel, get_peft_model
from transformers import AutoModelForCausalLM

base = AutoModelForCausalLM.from_pretrained("HuggingFaceTB/SmolLM2-135M")
config = LoraConfig(
    r=16, lora_alpha=32, lora_dropout=0.05,
    target_modules=["q_proj", "v_proj"], task_type="CAUSAL_LM",
)
model = get_peft_model(base, config)
model.print_trainable_parameters()
# trainable params: ... || all params: ... || trainable%: well under 1%

# ... train with Trainer(model=model, ...) as usual, lr around 2e-4 ...

model.save_pretrained("smol-lora")            # adapter only: a few MB

base = AutoModelForCausalLM.from_pretrained("HuggingFaceTB/SmolLM2-135M")
tuned = PeftModel.from_pretrained(base, "smol-lora")
merged = tuned.merge_and_unload()             # plain model, adapter folded in
merged.save_pretrained("smol-merged")

Remember

  • LoRA freezes base weights and trains small low-rank matrices
  • target_modules must match the architecture's layer names (or use all-linear)
  • Adapters save in MBs; merge_and_unload folds them in for deployment

Watch out for

  • Copying target_modules=['q_proj', 'v_proj'] to a BERT model, where those layer names do not exist, which raises an error.

Check yourself

  1. After get_peft_model with LoRA, which weights receive gradient updates?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: huggingface.co/docs/transformers/peft