2024-11-05 · 3 min read

QLoRA: 4-bit Training on Modest GPUs

QLoRA makes fine-tuning practical on consumer hardware by quantizing the base model to 4 bits while keeping the LoRA matrices in higher precision. The result is a dramatic reduction in memory footprint with negligible quality loss.

How QLoRA works

Instead of loading a 7B model in fp16 (14 GB), QLoRA loads it in NF4 (4-bit normalized float), using only ~5 GB. The frozen weights stay quantized. The LoRA matrices stay in bf16, so gradients and optimizer state remain high precision where it matters.

The math: a 7B model in fp16 needs ~28 GB for gradients and ~56 GB for AdamW optimizer state. With 4-bit quantization, the base model alone drops to ~2 GB, leaving room for training on a 12-16 GB card.

Memory requirements by model size

Base model FP16 INT8 NF4 NF4 + gradients + optimizer
7B 14 GB 8 GB 5 GB ~8–9 GB total
8B 16 GB 9 GB 5.5 GB ~9–10 GB total
13B 26 GB 15 GB 9 GB ~13–14 GB total

An RTX 3060 (12 GB) can train a 7B LoRA with QLoRA comfortably; an RTX 4090 can handle 13B.

Setup with bitsandbytes

The implementation relies on bitsandbytes for 4-bit quantization. Here's a complete example:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, TrainingArguments

# Quantization config
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,  # nested quantization
)

# Load base model quantized
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B-Instruct",
    quantization_config=bnb_config,
    device_map="auto",
)

# LoRA config
lora_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)

# Training
trainer = SFTTrainer(
    model=model,
    args=TrainingArguments(
        output_dir="./qlora-output",
        num_train_epochs=3,
        per_device_train_batch_size=1,
        gradient_accumulation_steps=8,
        learning_rate=2e-4,
        lr_scheduler_type="cosine",
    ),
    train_dataset=dataset,
)

trainer.train()

Quality impact

Training a Qwen2.5-7B adapter with QLoRA vs fp16 LoRA on the same dataset typically shows <1% downstream task degradation. For style and tone adapters, the difference is imperceptible. For knowledge-intensive tasks (e.g., structured extraction), the gap may be slightly larger but still acceptable.

The trade-off is worth it: 2–3x faster training, 6–7x lower VRAM usage, zero loss in released adapter quality.

When QLoRA is overkill

If your base model is already small (2–3B parameters), the quantization overhead becomes noticeable. A 3B model in fp16 is only 6 GB; standard LoRA fits on most GPUs without tricks.

For very narrow adapters where you're sensitive to every bit of fidelity (e.g., legal document extraction), fp16 LoRA is still safer.

Deployment note

A QLoRA adapter is just a LoRA adapter — it's distribution-agnostic. Once trained, it loads into a full-precision base model at inference time with no quality penalty. The quantization is only for training efficiency.

For ModelForgeLab, QLoRA adapters are marked with a badge during the training phase, but the released artifact is standard LoRA. The distinction helps users understand that a particular adapter was trained frugally on consumer hardware without compromising quality.

Practical command

mfl train qwen25-7b-support-tone \
  --method qlora \
  --rank 8 \
  --epochs 3 \
  --batch-size 1 \
  --gradient-accumulation 8 \
  --upload data/support_pairs.jsonl

QLoRA removes the barrier to adapter training. A laptop with an RTX 3060 can now train adapters that were previously possible only on A100s. That democratization is the point.