Grimoire
Sheet
pathfine-tuning/peft
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.

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+3264256    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