Low-rank deltas, frozen bases, QLoRA NF4, DoRA magnitude–direction decomposition, IA3 — and how to serve multiple adapters from one GPU.
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.
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.
The forward pass through a LoRA-patched linear layer is:
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 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.
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 %.
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 pattern | Layer type | Params targeted (7B) | Typical choice? |
|---|---|---|---|
q_proj, k_proj, v_proj, o_proj | Attention projections | ~8 M per layer | Yes — baseline |
gate_proj, up_proj, down_proj | MLP / FFN (SwiGLU) | ~22 M per layer | Yes for strong domain shift |
embed_tokens | Input embedding | ~524 M (vocab × d) | Only for new vocabulary |
lm_head | Output projection | ~524 M | Only for new token types |
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.
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.
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.
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.
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.
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)
)
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.
| Component | What it is | Why 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 Quantisation | The 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 Optimiser | NVIDIA 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. |
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
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.
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.
| Method | Trainable params (7B, r=16) | Commonsense reasoning gain vs LoRA | Extra 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 FT | 7 000 M (100 %) | +4–6 % (ceiling) | 14× VRAM |
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.
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.
For each attention layer, introduce three learned vectors:
Element-wise multiply: k' = l_k ⊙ k. Initialise all l to ones (identity at t=0).
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.
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 %
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).
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.
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)
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.
| System | Max concurrent adapters | Adapter source | Merge required? |
|---|---|---|---|
| vLLM ≥0.4 | Hundreds (paged) | HuggingFace Hub or local | No |
| LoRAX | Thousands (offload to CPU) | S3 / local | No |
| Ollama | 1 per instance | GGUF merged model | Yes (into GGUF) |
| llama.cpp | 1 per load | .gguf adapter file | Optional |
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.
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.