Fine-Tuning with LoRA and QLoRA

AIMachine LearningFine-TuningLoRA
Share on LinkedIn

Full fine-tune of a 13B model wanted eighty gigabytes of VRAM your team does not have. QLoRA loaded the base in 4-bit NormalFloat, attached rank-16 adapters to attention projections, and trained on a single A10 overnight — ninety-three percent of the domain metric gain at a fraction of the memory. LoRA (Low-Rank Adaptation) and QLoRA are how most teams actually fine-tune open models in 2025: freeze the pretrained weights, inject trainable low-rank matrices into selected layers, backprop only through adapters. The art is rank, target module selection, and knowing when to merge for production.

LoRA mechanics

For weight matrix W, LoRA adds ΔW = BA where B ∈ ℝ^{d×r}, A ∈ ℝ^{r×k}, r ≪ min(d,k):

from peft import LoraConfig, get_peft_model

config = LoraConfig(
    r=16,
    lora_alpha=32,       # scaling: alpha/r applied to LoRA output
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(base_model, config)
model.print_trainable_parameters()
# trainable params: ~0.5% of total typical for r=16 on 7B

Effective scaling lora_alpha / r — common pattern alpha = 2r.

QLoRA setup

Load base in 4-bit, attach LoRA in bf16/fp16:

import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig

bnb_config = 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(
    "meta-llama/Llama-3.1-8B",
    quantization_config=bnb_config,
    device_map="auto",
)

model = get_peft_model(model, config)
model.gradient_checkpointing_enable()

NF4 + double quant preserves quality for fine-tuning per QLoRA paper; train adapters in higher precision.

Memory estimation (rough)

QLoRA 7B:

Reduce max_seq_length, increase gradient_accumulation_steps before giving up.

Training loop essentials

from trl import SFTConfig, SFTTrainer

args = SFTConfig(
    output_dir="./lora-out",
    learning_rate=2e-4,
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    num_train_epochs=2,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    bf16=True,
    logging_steps=10,
    save_strategy="epoch",
    optim="paged_adamw_8bit",  # QLoRA-friendly
)

trainer = SFTTrainer(model=model, args=args, ...)
trainer.train()
trainer.save_model("lora-out/final")

paged_adamw_8bit reduces optimizer memory spikes.

Rank and module tuning

Symptom Try
Underfit domain Increase r to 32, add MLP modules
Forgetting general Decrease r, lower LR, fewer epochs
OOM r=8, shorter seq, more accumulation
Slow convergence Slightly higher LR (cap 3e-4 LoRA)

Do not grid-search blindly — one eval metric per change.

Merge and deploy

Merge adapters into base for static deployment:

from peft import PeftModel

base = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B", torch_dtype=torch.bfloat16)
merged = PeftModel.from_pretrained(base, "lora-out/final")
merged = merged.merge_and_unload()
merged.save_pretrained("merged-model")

Or serve with LoRA hot-swap (vLLM):

vllm serve meta-llama/Llama-3.1-8B --enable-lora --lora-modules domain=lora-out/final

Version adapter artifacts alongside base model hash — incompatible base upgrades break adapters silently.

Pitfalls

LoRA/QLoRA democratized fine-tuning; discipline in eval and deployment separates demo from production.

Choosing rank and alpha

Rank (r) controls adapter capacity; alpha scales the adapter contribution:

Use case r alpha target_modules
Style/tone adjustment 8 16 q_proj, v_proj
Domain knowledge (7B) 16–32 32–64 all attention + MLP
Tool-use / function calling 32 64 all linear layers
Full behavior change 64+ 128 all linear layers

Rule of thumb: alpha = 2 × r. Higher rank increases trainable params and VRAM — 7B model with r=16 adds ~4M params vs r=64 adding ~16M.

LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                     "gate_proj", "up_proj", "down_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

For QLoRA, 4-bit base + r=16 typically fits 7B on 24GB GPU.

Multi-adapter serving

Production often needs domain-specific adapters on one base model:

# vLLM: load multiple LoRA adapters
vllm serve meta-llama/Llama-3.1-8B \
  --enable-lora \
  --lora-modules legal=./adapters/legal medical=./adapters/medical \
  --max-lora-rank 64

Route requests by domain header or classifier. Adapter swap at inference is microseconds — no model reload required.

Track adapter-base compatibility: document which base model hash each adapter was trained against. Upgrading base without retraining adapters causes silent quality degradation.

QLoRA vs full LoRA tradeoffs

QLoRA (4-bit base) LoRA (fp16 base)
VRAM (7B) ~10GB ~20GB
Training speed Slower (dequant overhead) Faster
Final quality 95–98% of full LoRA Baseline
Merge to fp16 Required before merge Direct merge

For experimentation and iteration, QLoRA wins on hardware cost. For final production adapter, consider full LoRA fine-tune on the best hyperparameters found via QLoRA.

Failure modes

Production checklist

Merge LoRA adapters and run regression eval before production deploy — merged weights behave differently than adapter-at-inference paths.

Track adapter rank and target module choices in experiment metadata — rank-8 on q_proj only vs rank-16 on all attention layers produces incomparable eval results across runs.

Resources

Frequently asked questions

What GPU memory do I need for QLoRA fine-tuning a 7B model?

QLoRA on 7B typically fits in 16–24 GB VRAM with batch size 1–4, sequence length 2048, r=16, and gradient checkpointing — varies by implementation. Full fine-tuning 7B often needs 4×+ memory. 70B QLoRA may require 48 GB+ or multi-GPU with FSDP.

Which layers should LoRA target?

Common defaults: attention q_proj, k_proj, v_proj, o_proj and sometimes MLP gate/up/down projections. Larger rank on attention only often suffices; adding MLP adapters increases capacity and memory. Match target_modules to your model architecture strings in PEFT config.

Should I merge LoRA weights into the base model for deployment?

Merge for lowest inference latency and simplest serving (single weight file). Keep adapters separate for multi-tenant customization (swap LoRA per customer) and smaller artifact storage. vLLM and TGI support hot-swapping LoRA adapters without merging.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →