Fine-Tuning & PEFT Series — Presentation 02

LoRA and PEFT Variants

Low-rank deltas, frozen bases, QLoRA NF4, DoRA magnitude–direction decomposition, IA3 — and how to serve multiple adapters from one GPU.

LoRA QLoRA DoRA IA3 PEFT NF4 Multi-LoRA Adapter Merging
Frozen Base W → A (r × d) → B (d_out × r) → ΔW = BA → W + (α/r)ΔW → Merge / Serve
00

Topics We'll Cover

01

The LoRA Idea — Low-Rank Deltas, Frozen Base

Hu et al. (2021) — LoRA: Low-Rank Adaptation of Large Language Models — started from an empirical observation: when a large model is fine-tuned on a downstream task, the weight update matrix ΔW has intrinsically low rank. The pre-training process has already found a high-dimensional representation; the task-specific delta lives in a much lower-dimensional subspace.

If that's true, instead of adding a full-rank ΔW ∈ Rdout×din (which would be 4–16 GB of additional parameters for a 7B model), you can represent it as a product of two thin matrices whose combined parameter count is tiny.

Pre-trained W
d_out × d_in
frozen, not updated
+
B · A
(d_out × r) · (r × d_in)
trainable, r ≪ d
=
W + (α/r) B·A
effective weight at inference
Why this matters

For a Llama-3 8B attention projection of shape 4096×4096 = 16.8 M parameters, a rank-16 LoRA uses only 2×(4096×16) = 131 k parameters — 0.78 % of the original. A full 7B model's LoRA adapters at rank 16 targeting all q/k/v/o projections occupy roughly 20–30 MB vs 14 GB for full fine-tuning. VRAM for the optimiser states shrinks proportionally.

02

LoRA Math — A·B with Rank r, Scaling α

The forward pass through a LoRA-patched linear layer is:

h = W 0 x + (α / r) · B · A · x frozen pretrained weight matrix scaling factor (α = r → no scale) trainable rank-r decomposition B init = zeros, A init = kaiming_uniform

Key initialisations: A is initialised with Kaiming uniform so the scale of the random projection is sensible; B is initialised to zeros so that at the start of training the LoRA delta contributes nothing — the model begins training identical to the frozen base, which stabilises early loss curves.

The α / r scaling convention

The scaling factor α/r normalises the magnitude of the LoRA update. If you double the rank without changing α, the effective update doubles in magnitude — the learning rate and α are entangled. Common practice: set α = r (scale = 1, no amplification) or α = 2r (doubles the effective update). The HF PEFT library defaults to alpha=16, rank=8 giving a 2× amplification.

Parameter count formula

Total LoRA params = ∑layers (din × r + r × dout) across all targeted modules. For Llama-3 8B with r=16 targeting all q, k, v, o projections in all 32 layers: 32 × 4 × 2 × 4096 × 16 = 16.8 M trainable params out of 8.03 B total — about 0.21 %.

03

Target Modules — q/k/v/o_proj & MLPs

LoRA can be applied to any linear layer. In practice the choice of which layers to target is the most impactful design decision after rank.

Module patternLayer typeParams targeted (7B)Typical choice?
q_proj, k_proj, v_proj, o_projAttention projections~8 M per layerYes — baseline
gate_proj, up_proj, down_projMLP / FFN (SwiGLU)~22 M per layerYes for strong domain shift
embed_tokensInput embedding~524 M (vocab × d)Only for new vocabulary
lm_headOutput projection~524 MOnly for new token types

Module Names by Architecture

Llama-3
q_proj k_proj v_proj o_proj gate_proj up_proj down_proj
Mistral
q_proj k_proj v_proj o_proj w1 w2 w3
Qwen-2.5
q_proj k_proj v_proj o_proj up_proj down_proj gate_proj
Axolotl target_modules shorthand

Axolotl accepts target_modules: all-linear to LoRA all linear layers. This is thorough but increases VRAM and training time. For most instruction-tuning tasks, just the 4 attention projections is sufficient. Add MLP layers if you're doing significant domain adaptation (e.g. medical, legal) where the FFN's factual recall needs updating.

04

Hyperparameters — Rank 8 vs 64 vs 128, α, Dropout

LoRA has fewer hyperparameters than full fine-tuning, but the ones it has are sensitive. Here is what the research and community practice have converged on.

Rank (r)

r = 8: chat & instruction following, minimal domain shift. Smallest VRAM overhead.

r = 16–32: moderate domain adaptation; code, math, moderate specialisation.

r = 64–128: large domain shift; medical, legal, multilingual. Diminishing returns above 128 for most tasks.

Alpha (α)

Controls effective update magnitude via scale α/r.

Common defaults: α = r (PEFT docs), α = 16 fixed (many tutorials). Best practice: treat α as a learning-rate multiplier. If loss is slow to decrease, raise α; if unstable, lower it.

RSLoRA (2023) proposes α/√r normalisation for better stability at high ranks.

Dropout

Applied to the LoRA A matrix output before B. Reduces overfitting on small datasets.

Default: 0.0 (no dropout). Set to 0.05–0.1 for datasets <10 k examples or when eval loss diverges from train loss after epoch 1.

Has minimal effect on large datasets (>100 k examples) — skip it and save compute.

PEFT LoraConfig — recommended starting point for 7B instruction tuning
from peft import LoraConfig, TaskType

config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,
    lora_alpha=16,           # scale = 1.0
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0.05,
    bias="none",              # "all" or "lora_only" are rarely better
    use_rslora=True,           # alpha/sqrt(r) normalisation (PEFT ≥0.6)
)
05

QLoRA — 4-bit Base, NF4, Double Quant

Dettmers et al. (2023) — QLoRA: Efficient Finetuning of Quantized LLMs — combined LoRA with 4-bit weight quantisation. The three technical innovations are tightly coupled and each is necessary for the approach to work without accuracy collapse.

ComponentWhat it isWhy it matters
NF4 (NormalFloat4)An information-theoretically optimal 4-bit data type for normally-distributed weights. 16 quantisation levels spaced to minimise expected quantisation error under a Gaussian prior.Normal 4-bit integer quantisation loses 0.5–1 perplexity on LLMs; NF4 closes >80 % of that gap.
Double QuantisationThe per-block quantisation scale constants (themselves fp32) are quantised again with 8-bit floats. Saves ~0.37 bits per parameter extra.Reduces the overhead of storing 32-fp32 scale constants per 64-element block from 0.5 bits/param to ~0.13 bits/param.
Paged OptimiserNVIDIA unified memory (CUDA managed memory) pages GPU memory to CPU RAM on overflow. Uses bitsandbytes paged AdamW.Prevents OOM crashes during backward when a single step spikes above available VRAM, at the cost of latency on the spill steps.
bitsandbytes + PEFT QLoRA setup (transformers ≥4.36)
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
import torch

bnb_cfg = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,  # dequantise to bf16 for compute
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    quantization_config=bnb_cfg,
    device_map="auto",
)
# Then wrap with PEFT LoRA as normal; gradients flow through bf16 adapters
QLoRA VRAM budget (Llama-3 8B)

Base model at NF4: 8B × 0.5 bytes ≈ 4.0 GB. LoRA adapters r=16, all attn: ~130 MB. Activations (batch=4, seq=2048): ~1.5 GB. Optimiser states (bf16 adapters): ~260 MB. Total: ~6 GB — fits a single RTX 3080 10 GB with room to spare.

06

DoRA — Magnitude-Direction Decomposition

Liu et al. (2024) — DoRA: Weight-Decomposed Low-Rank Adaptation — argued that LoRA fails to match full fine-tuning because it conflates two independent properties of each weight matrix: its magnitude (the column-wise L2 norm) and its direction (the normalised column vectors).

DoRA decomposes the pretrained weight as W0 = m · V / ||V|| where m ∈ R1×k is the magnitude vector (one scalar per column) and V ∈ Rd×k is the direction. Only the direction update is low-rank (LoRA); the magnitude is a separate trainable scalar per column.

W' = m · (W 0 + BA ) / ||W 0 + BA || m: trainable magnitude (1 × k scalars) — tiny extra cost BA: LoRA rank-r direction update (standard LoRA term) Result: separates learning "how big" from "in which direction" — more stable on complex tasks
MethodTrainable params (7B, r=16)Commonsense reasoning gain vs LoRAExtra cost
LoRA~16.8 M (0.21 %)baseline—
DoRA~17.2 M (0.215 %)+1–3 % on LLaMA commonsense benchmarks~1 % extra VRAM for m vectors
Full FT7 000 M (100 %)+4–6 % (ceiling)14× VRAM
Using DoRA in PEFT

Set use_dora=True in LoraConfig (PEFT ≥0.9). DoRA is transparent to the rest of the training loop — same trainer, same merging. Axolotl 0.4+ supports peft_lora_dora: true. Most useful for tasks with significant style or capability gap from the base model; marginal for light instruction-tuning.

07

IA3 — Even Smaller Perturbations

Liu et al. (2022) — Few-Shot Parameter-Efficient Fine-Tuning is Better and Cheaper than In-Context Learning — introduced IA3 (Infused Adapter by Inhibiting and Amplifying Inner Activations). Instead of adding a rank-r matrix delta, IA3 multiplies key, value, and FFN activation streams by learned scalar vectors.

IA3 Mechanics

For each attention layer, introduce three learned vectors:

  • lk ∈ Rd_k — scales the key projections
  • lv ∈ Rd_v — scales the value projections
  • lff ∈ Rd_ff — scales the intermediate FFN activations

Element-wise multiply: k' = l_k ⊙ k. Initialise all l to ones (identity at t=0).

Comparison to LoRA

IA3 parameter count for a 7B model: ~3 vectors × 32 layers × d ≈ 400 k parameters total — roughly 40× fewer than LoRA at r=16.

Quality: approaches LoRA on many few-shot classification tasks; lags behind on open-ended generation where richer representational change is needed.

Best fit: extremely constrained serving environments, few-shot classification, tasks with well-defined closed output spaces.

PEFT IA3Config
from peft import IA3Config, TaskType, get_peft_model

config = IA3Config(
    task_type=TaskType.CAUSAL_LM,
    target_modules=["k_proj", "v_proj", "down_proj"],
    feedforward_modules=["down_proj"],  # which modules are feedforward
)
model = get_peft_model(base_model, config)
model.print_trainable_parameters()
# trainable params: 401,408 || all params: 8,030,000,000 || 0.005 %
08

Merging Adapters & Multi-LoRA Serving

Once training is complete there are two distinct deployment patterns: merge-and-serve (fold the adapter into the base weights permanently) or hot-swap serving (keep the base frozen and load adapters per request at inference time).

Merge (Linear Combination)

The merged weight is simply Wmerged = W0 + (α/r) · B · A. This produces a single model identical at inference to LoRA but with zero adapter overhead. PEFT handles this with model.merge_and_unload(). The result is a standard HuggingFace model you can push to Hub.

peft merge_and_unload + safe serialise
from peft import PeftModel

base   = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3-8B")
model  = PeftModel.from_pretrained(base, "./lora-adapter-dir")
merged = model.merge_and_unload()   # returns a plain AutoModelForCausalLM
merged.save_pretrained("./merged-model", safe_serialization=True)

Multi-LoRA Serving

Frameworks like vLLM (v0.4+) and LoRAX load one base model into GPU VRAM and hot-swap per-request adapter weights from CPU RAM or disk. Because only the LoRA A/B matrices need to change (<1 % of model size), adapter swap overhead is minimal — typically <5 ms for r=16.

SystemMax concurrent adaptersAdapter sourceMerge required?
vLLM ≥0.4Hundreds (paged)HuggingFace Hub or localNo
LoRAXThousands (offload to CPU)S3 / localNo
Ollama1 per instanceGGUF merged modelYes (into GGUF)
llama.cpp1 per load.gguf adapter fileOptional
Adapter soup / model merging

TIES-merging and DARE (both in mergekit) let you linearly combine multiple LoRA adapters trained on different tasks into a single merged model — analogous to model ensembling but at the weight level. This is distinct from multi-LoRA serving: here you want one merged model that retains properties from all source adapters, trading some peak performance per task for breadth.

09

What to Take Away

Where to next

Deck 03 confronts what SFT and LoRA cannot fix: the model that gives technically correct but unhelpful, misleading, or harmful outputs. That's the problem RLHF and PPO were designed to solve.