LoRA and QLoRA

LoRA and QLoRA make it practical to fine-tune large language models on a single GPU. This page explains the low-rank idea, the memory tricks QLoRA adds, a working PEFT setup and the mistakes that most often waste a training run.


1. What LoRA and QLoRA Are

LoRA (Low-Rank Adaptation, Hu et al., 2021) is a way to fine-tune a large model without updating its original weights. Each chosen weight matrix W is frozen, and a small trainable correction is added beside it, written as the product of two thin matrices B and A. Only those thin matrices are trained, so the number of trainable parameters typically drops by a factor of hundreds or more.

QLoRA (Dettmers et al., 2023) goes further by storing the frozen base model in 4-bit precision while training LoRA adapters in 16-bit. The QLoRA paper showed a 65-billion-parameter model fine-tuned on a single 48 GB GPU. Both belong to the family of parameter-efficient fine-tuning (PEFT) methods, which also includes adapters, prefix tuning and prompt tuning.

2. Why It Matters

Full fine-tuning updates every weight and, with the Adam optimizer in mixed precision, needs roughly 16 bytes of GPU memory per parameter for weights, gradients and optimizer state — on the order of 100 GB or more for a 7-billion-parameter model before counting activations. LoRA removes gradients and optimizer state for the frozen weights, and QLoRA shrinks the frozen weights themselves to about half a byte per parameter. That moves fine-tuning from multi-GPU clusters to a single workstation GPU.

There is an operational benefit too. An adapter for a 7B model is often only tens of megabytes. One base model can serve many tasks or customers by swapping adapters, and serving engines such as vLLM can batch requests for different adapters together.

3. How It Works

LoRA: frozen weight plus low-rank update input x d dims W (frozen) d x k, e.g. 4096 x 4096 A r x k B d x r, init 0 + h = Wx + BAx scaled by alpha/r Only A and B are trained: r(d + k) parameters instead of d x k.

For a frozen weight W of shape dxk, LoRA learns A (rxk) and B (dxr) with a small rank r, typically 4 to 64. The layer computes

h = W x + (α / r) · B A x

A starts random and B starts at zero, so at step zero the model behaves exactly like the base model. The scaling factor α/r keeps the update size roughly stable when you change the rank. The bet behind LoRA is that the change needed to adapt a pretrained model has low intrinsic rank, an idea supported by earlier work on intrinsic dimensionality (Aghajanyan et al., 2021).

Trainable parameters for one 4096 x 4096 matrix full: 16.8M r = 64: 524K r = 16: 131K r = 8: 65.5K r = 8 trains about 0.4% of the matrix (2 x 4096 x 8 / 4096^2).

After training, BA can be added into W ("merged"), so inference costs exactly the same as the base model. Adapters are usually applied to the attention projections (q_proj, k_proj, v_proj, o_proj); the QLoRA paper found that adding them to all linear layers, including the MLP, was needed to match full fine-tuning quality.

QLoRA memory recipe Base model in 4-bit NF4 frozen, double-quantized ~0.5 bytes / param LoRA adapters in BF16 trainable tiny fraction of params Paged AdamW optimizer state on adapters spills to CPU on spikes Forward: dequantize 4-bit block to BF16 on the fly, matmul, discard

QLoRA adds three ideas. 4-bit NormalFloat (NF4) is a data type whose 16 levels are spaced for normally distributed weights. Double quantization also quantizes the per-block scaling constants, saving roughly another 0.4 bits per parameter. Paged optimizers use unified memory so optimizer state can spill to CPU RAM during memory spikes instead of crashing. Gradients flow through the dequantized 4-bit weights into the 16-bit adapters; the base weights never change.

4. Worked Example with Hugging Face PEFT

A minimal QLoRA setup with transformers, peft and bitsandbytes (NVIDIA GPU required for 4-bit). Swap the model id for any causal LM you have access to.

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

model_id = "Qwen/Qwen2.5-0.5B"   # small enough to try; same code for 7B+

bnb = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)
tok = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, quantization_config=bnb,
                                             device_map="auto")
model = prepare_model_for_kbit_training(model)   # enables grad checkpointing, casts norms

lora = LoraConfig(
    r=16, lora_alpha=32, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM",
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
)
model = get_peft_model(model, lora)
model.print_trainable_parameters()   # prints trainable vs total parameter counts

# ... train with transformers.Trainer or trl.SFTTrainer on your dataset ...
model.save_pretrained("my-adapter")  # saves only the adapter weights

To serve, either load the base model and attach the adapter with PeftModel.from_pretrained(base, "my-adapter"), or merge with merge_and_unload() on a 16-bit copy of the base model and export a normal checkpoint. Merging into a 4-bit model is lossy, so merge in 16-bit.

5. Choosing Rank, Alpha and Data

Most LoRA runs only need decisions on a handful of settings. Rank controls capacity: 8 or 16 is enough for teaching a format or tone, while harder domain shifts such as a new programming language or many specialised tasks may benefit from 32 to 64. Raising rank beyond what the data supports mainly adds overfitting risk. Alpha is commonly set to the rank or twice the rank; because the update is scaled by alpha divided by rank, changing one without the other changes the effective learning rate. Dropout of 0.05 to 0.1 on the adapter path helps with small datasets.

Data quality matters far more than these knobs. A few thousand clean, consistent, representative examples usually beat a much larger noisy set. Hold out a validation split, watch validation loss for the overfitting pattern described on the regularization page, and compare generations from several checkpoints side by side, because loss alone does not reveal whether answers became better or just shorter. Keep a fixed set of general-purpose prompts as a regression test for forgetting.

6. Comparison with Other Fine-Tuning Methods

MethodTrainable paramsMemoryInference overheadNotes
Full fine-tuning100%HighestNoneBest ceiling; one full copy per task
LoRATypically well under 1%Base weights in 16-bit, no optimizer state for themNone if mergedMost widely used PEFT method
QLoRASame as LoRABase weights in 4-bitDequantization cost unless mergedSlower per step than LoRA; fits much larger models
DoRASlightly more than LoRASimilar to LoRANone if mergedSplits magnitude and direction; often small gains
Adapters (Houlsby)SmallLowExtra layers at inferenceEarlier PEFT method
Prompt / prefix tuningTinyLowestLonger effective contextWeaker on smaller models

7. Pitfalls

9. Further Reading