ExperimentsFinetuning

Fine-Tuning Large Language Models: PEFT, LoRA, and QLoRA in Practice

Full parameter training requires clusters of H100s. Parameter-Efficient Fine-Tuning (PEFT) with low-rank matrix decomposition brings specialized adaptation to a single GPU.

Kushan Manahara

December 18, 2024 · 4 min read

00
Fine-Tuning Large Language Models: PEFT, LoRA, and QLoRA in Practice

Foundation models such as Llama 3, Mistral, and Gemma possess immense world knowledge, but when dropped into production pipelines, they often struggle with specialized tasks: adhering to strict JSON schemas, matching proprietary brand tone, or executing multi-step domain reasoning without lengthy, expensive few-shot system prompts.

Engineers often jump straight to fine-tuning when they shouldn't. Before touching weights, follow this production decision hierarchy:

  • Prompt Engineering & Few-Shot: Best for rapid iteration and testing domain feasibility.
  • Retrieval-Augmented Generation (RAG): Essential when knowledge is volatile, external, requires citations, or updates daily.
  • Fine-Tuning: Necessary when you need to teach a model how to behave rather than what to know—e.g., teaching custom tool syntax, reducing 2,000-token prompt instructions into weight memory to save latency, or mastering esoteric programming languages.

The VRAM Wall: Why Full Fine-Tuning Fails

In full-parameter fine-tuning, every single parameter is updated. For a 7-billion parameter model in 16-bit precision (bfloat16):

  • Model weights: 14 GB
  • Gradients: 14 GB
  • AdamW optimizer states (FP32 master weights, momentum, and variance): 56 GB (8 bytes per parameter)
  • Activation memory: 10–20 GB depending on sequence length and batch size

Total VRAM required: over 90 GB, demanding multiple $30,000 enterprise GPUs. This economic barrier led to the invention of Parameter-Efficient Fine-Tuning (PEFT).

The Mathematics of LoRA (Low-Rank Adaptation)

Proposed by Edward Hu et al. at Microsoft, LoRA hypothesizes that the weight updates during task adaptation have a low 'intrinsic rank'. Instead of modifying the full weight matrix W₀ ∈ ℝ^(d×k), LoRA freezes W₀ and parameterizes the update ΔW as the product of two low-rank matrices:

lora_decomposition.txt
W = W_0 + ΔW = W_0 + (α / r) * (B × A)

Where:
  W_0 ∈ ℝ^(d × k)  [Frozen base weights]
  A   ∈ ℝ^(r × k)  [Gaussian initialized, e.g. N(0, σ²)]
  B   ∈ ℝ^(d × r)  [Initialized to zero, so ΔW starts at 0]
  r   ≪ min(d, k)  [Rank parameter, typically 8, 16, or 32]
  α   = Scaling hyperparameter (constant factor)

For a weight matrix of dimensions 4096×4096 (16.7 million parameters), a LoRA adapter with rank r = 16 trains only 2 × 16 × 4096 = 131,072 parameters—a 99.2% reduction in trainable parameters. During inference, the product ΔW = (α/r)(B × A) can be mathematically merged back into W₀, resulting in zero inference latency penalty.

QLoRA: Fine-Tuning on Consumer GPUs

QLoRA (Dettmers et al.) pushed efficiency further by quantizing the base model W₀ to 4-bit NormalFloat (NF4) and using Double Quantization and Paged Optimizers. This allows an engineer to fine-tune a full Llama 3 8B model on a single 16GB GPU (like an RTX 4080 or T4).

train_lora.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig

model_id = "meta-llama/Meta-Llama-3-8B-Instruct"

# 1. 4-bit 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,
)

tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto",
)

# 2. Configure LoRA parameters
model = prepare_model_for_kbit_training(model)
peft_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# Output: trainable params: 13,631,488 || all params: 8,043,892,736 || trainable%: 0.169%

Written by

Kushan Manahara

Responses (0)

Verified name, role, and email required before posting.

No responses yet

Be the first to share your thoughts, benchmarks, or feedback above.