QLoRA: LoRA on a 4-bit base

Hugging Face Transformers · Advanced · 14 min

QLoRA is the standard recipe for fine-tuning 7B to 70B models on modest hardware.

QLoRA loads the base model in 4-bit NF4 with bitsandbytes and trains LoRA adapters in higher precision on top. The frozen base costs a quarter of its bf16 memory, and gradients flow only into the small adapters, so a 7B model fine-tunes comfortably on a 24 GB GPU.

The recipe:

1. Load the base with a 4-bit BitsAndBytesConfig (nf4, bf16 compute, double quantization). 2. Call prepare_model_for_kbit_training(model) from PEFT. It enables gradient checkpointing, makes inputs require gradients so checkpointing works with a frozen base, and casts layer norms to float32 for stability. 3. Apply LoraConfig with target_modules="all-linear"; adapting every linear layer recovers most of full fine-tuning quality. 4. Train with Trainer using bf16=True, an optimizer such as optim="paged_adamw_8bit" to cut optimizer memory, and a learning rate around 2e-4.

For deployment you cannot directly merge an adapter into 4-bit weights without precision loss. The usual approach is to reload the base model in bf16, load the adapter, merge_and_unload(), and then optionally quantize the merged model again with a method suited to inference.

For supervised instruction tuning, the companion TRL library's SFTTrainer wraps this recipe: it accepts a peft_config, applies chat templates and packs sequences.

Example

import torch
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import AutoModelForCausalLM, BitsAndBytesConfig, TrainingArguments

bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
                         bnb_4bit_compute_dtype=torch.bfloat16,
                         bnb_4bit_use_double_quant=True)
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B", quantization_config=bnb, device_map="auto")

model = prepare_model_for_kbit_training(model)   # grad checkpointing, fp32 norms
model = get_peft_model(model, LoraConfig(
    r=16, lora_alpha=32, lora_dropout=0.05,
    target_modules="all-linear", task_type="CAUSAL_LM"))

args = TrainingArguments(
    output_dir="qwen-qlora", per_device_train_batch_size=4,
    gradient_accumulation_steps=4, learning_rate=2e-4, num_train_epochs=1,
    bf16=True, optim="paged_adamw_8bit", logging_steps=10, report_to="none",
)
# Trainer(model=model, args=args, train_dataset=..., data_collator=...).train()

Remember

  • QLoRA = frozen 4-bit NF4 base + LoRA adapters trained in bf16
  • Call prepare_model_for_kbit_training before adding adapters
  • Merge adapters into a bf16 reload of the base, not into 4-bit weights

Watch out for

  • Merging an adapter directly into the 4-bit base and shipping it, which loses precision compared with merging into bf16 weights.

Check yourself

  1. Why does QLoRA use NF4 for the base but bf16 for the adapters?

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

Official docs: huggingface.co/docs/transformers/quantization/bitsandbytes

Builds on

Unlocks

Nothing builds on this yet.