AI Grimoire
Sheet
statusstandard
difficultyadvanced
timeO(r·d)
described2023
revised2w ago

QLoRA

Quantise the frozen base to 4 bits, keep the adapters in bf16, and backpropagate through the dequantisation. Three tricks make the accuracy loss negligible.

Standing

Current standardPart of the default recipe. Present in most frontier models trained today, and the thing a new design departs from rather than argues for.

How fine-tuning happens on one GPU. The quantised base plus low-rank update combination is now routine.

judged as of 2026-09 · what the labels mean

Theory

NF4 is an information-theoretically optimal code for zero-centred normal data: its sixteen levels are the quantiles of a standard normal, so each level receives equal probability mass under the assumption that pretrained weights are approximately Gaussian per block.

qi=12[Φ−1 ⁣(i17)+Φ−1 ⁣(i+117)]/Φ−1 ⁣(1617)q_i = \frac{1}{2}\left[ \Phi^{-1}\!\left(\frac{i}{17}\right) + \Phi^{-1}\!\left(\frac{i+1}{17}\right) \right] \Big/ \Phi^{-1}\!\left(\frac{16}{17}\right)
eq. 1 — NF4 levels, normalised to [−1, 1]

Double quantisation then quantises the per-block scale factors themselves, which at block size 64 costs 0.5 bits per parameter before compression and 0.127 after. Paged optimiser state moves Adam moments to host memory on gradient-checkpointing spikes rather than failing the allocation.

4+3264  ⟶  4+864+3264⋅256  ≈  4.127  bits/param4 + \tfrac{32}{64} \;\longrightarrow\; 4 + \tfrac{8}{64} + \tfrac{32}{64 \cdot 256} \;\approx\; 4.127 \;\text{bits/param}
eq. 2 — before and after double quantisation

The base weights never leave 4 bits in memory; they are dequantised into bf16 tile by tile inside the matmul. This is why QLoRA is compute-bound where LoRA is not — you pay a dequantisation per forward and per recompute.

Implementation

python · transformers + peft
import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model

bnb = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B", quantization_config=bnb
)
model.gradient_checkpointing_enable()

model = get_peft_model(model, LoraConfig(
    r=64, lora_alpha=16, lora_dropout=0.05,
    target_modules="all-linear",   # every projection, not just q/v
))

Keep compute_dtype at bf16; fp16 reintroduces the overflow problems that the NF4 block scales were meant to remove.

Related

References

[1]Dettmers et al. — QLoRA: Efficient Finetuning of Quantized LLMs (2023)arXiv:2305.14314
[2]Dettmers et al. — 8-bit Optimizers via Block-wise Quantization (2021)arXiv:2110.02861