Hugging Face LoRA Fine-Tuning with PEFT

LoRA fine-tuning with Hugging Face PEFT lets you adapt a large language model by training tiny add-on matrices instead of all its weights. PEFT stands for parameter-efficient fine-tuning, and LoRA (low-rank adaptation) is its most popular method.

Instead of updating a 7-billion-parameter model, you might train only 10-40 million parameters. That means far less GPU memory, faster training and adapter files of a few megabytes instead of many gigabytes.

How LoRA Works

For selected weight matrices (usually the attention projections), LoRA freezes the original weight W and learns two thin matrices A and B. Their product B times A has the same shape as W but low rank r, often 8 or 16. During inference the model uses W plus a scaled B times A. Because r is small, A and B contain very few numbers.

Adding a LoRA Adapter

from transformers import AutoModelForCausalLM
from peft import LoraConfig, get_peft_model

model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct", dtype="auto")

config = LoraConfig(
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
trainable params: 2,162,688 || all params: 496,195,456 || trainable%: 0.4359

Less than half a percent of the weights will be trained. The exact counts depend on the model and on which modules you target; target_modules="all-linear" is a convenient option that adapts every linear layer.

Training the Adapter

The PEFT model is a normal PyTorch module, so you can train it with Trainer. Many people use the TRL library's SFTTrainer, which adds chat-template formatting for instruction datasets:

from datasets import load_dataset
from trl import SFTTrainer, SFTConfig

data = load_dataset("json", data_files="my_chats.jsonl")["train"]  # rows with a "messages" list
trainer = SFTTrainer(
    model=model,
    train_dataset=data,
    args=SFTConfig(output_dir="qwen-lora", num_train_epochs=1,
                   per_device_train_batch_size=4, learning_rate=2e-4),
)
trainer.train()
{'loss': 1.84, 'epoch': 0.25}
{'loss': 1.21, 'epoch': 0.5}
...
(sample output — losses vary)

LoRA usually uses a higher learning rate (around 1e-4 to 2e-4) than full fine-tuning because only the small adapter learns.

Saving, Loading and Merging

model.save_pretrained("qwen-lora/adapter")   # only a few MB

from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct", dtype="auto")
tuned = PeftModel.from_pretrained(base, "qwen-lora/adapter")

merged = tuned.merge_and_unload()     # fold the adapter into the weights
merged.save_pretrained("qwen-merged")
qwen-lora/adapter/adapter_config.json, adapter_model.safetensors written
qwen-merged/ written with full model weights

QLoRA and Practical Tips

  • QLoRA loads the frozen base model in 4-bit (see the quantization lesson) and trains LoRA on top, fitting 7B-8B models on a single 16-24 GB GPU.
  • Quality of your training data matters far more than the exact r value.
  • Keep a held-out set of prompts and compare answers before and after tuning.
  • Several adapters can share one base model and be switched at run time.

Frequently asked questions

What is LoRA in LLM fine-tuning?

Low-rank adaptation: the original weights are frozen and small low-rank matrices are trained and added to them, drastically reducing memory and storage needs.

What is the difference between LoRA and QLoRA?

QLoRA is LoRA applied to a base model that has been quantized to 4-bit, which lowers memory even further so larger models fit on smaller GPUs.

Should I merge a LoRA adapter?

Merge when you want a single standalone model for deployment with no extra latency. Keep it separate when you want to swap several adapters on one base model.