AI Fundamentals

QLoRA Guide

Engineering guide to QLoRA: 4-bit NF4 base weights, double quantization, paged optimizers, VRAM savings, and trade-offs vs BF16 LoRA — with Hugging Face PEFT and bitsandbytes.

55 min readAdvancedLast reviewed: 21 July 2026

Quick Summary

QLoRA fine-tunes LoRA adapters while storing the frozen base model in 4-bit NormalFloat, cutting training VRAM enough to adapt 70B-class models on a single large GPU.

One Analogy

The heavy textbook stays on a compressed shelf (4-bit weights); you only rewrite a thin notepad of adapters in high precision.

Engineering Rule

Use QLoRA to fit training in VRAM — not to speed it up — and never skip task eval in quality-sensitive domains before promoting a 4-bit-trained adapter.

TL;DR

  • QLoRA = frozen 4-bit base (usually NF4) + high-precision LoRA adapters trained with PEFT + bitsandbytes.
  • Three ideas: NF4 quantization, double quantization of quant constants, and paged optimizers to survive memory spikes.
  • VRAM drops ~4× on base weights vs BF16 LoRA; wall-clock per step is often similar or slower because of dequantization.
  • Quality on many SFT benchmarks stays close to 16-bit LoRA because gradients update adapters, not the quantized (W).
  • When NOT: extreme quality-sensitive domains without rigorous eval; non-CUDA environments; assuming 4-bit serving is the same decision as 4-bit training.
  • Still subordinate to fine-tuning boundaries: do not QLoRA a product catalog into weights — use RAG.

Quick Decision Guide

If you want to... Read
See all adapter methods PEFT
Use low-rank adapters LoRA
Train with limited VRAM QLoRA
Do full fine-tuning Fine-tuning
Change model knowledge instead RAG
Understand the base architecture Transformers

Who this guide is for

  • Best for: AI engineers · ML engineers · applied research engineers
  • Difficulty: Advanced
  • Estimated time: 55 min

Learning Path

Fine-tuningPEFTLoRAQLoRARLHFDPO

On this page

Why This Matters

A 70B parameter model in BF16 is ~140GB of weights alone. Classic LoRA shrinks trainable state but still loads the full-precision frozen base. QLoRA (Dettmers et al., 2023) stores that base in 4-bit, making single-GPU or few-GPU adaptation realistic for Llama 3.x / Llama 4-class and Mistral-family instruct models that teams actually deploy in 2026.

Without QLoRA, many orgs never leave API fine-tuning. With it, they must learn a sharper ops surface: bitsandbytes flags, prepare_model_for_kbit_training, paged AdamW, and honest evaluation against BF16 LoRA when quality bars are high.

The strategic effect is democratization with caveats. Researchers and startups can iterate on large open weights without an eight-GPU reservation. Enterprises can prototype private adapters before committing cluster spend. The caveat is cultural: once 70B fine-tunes become “easy,” quality review must become stricter, not looser. Cheap capacity without eval gates produces confident, domain-sounding failures — the worst kind for trust.

Engineering Insight

QLoRA trades a little precision for a lot of VRAM headroom: a 4-bit frozen base plus small adapters fits larger models on fewer GPUs. Validate quality on a held-out set before shipping.

The Problem QLoRA Solves

Model class BF16 weights (order) LoRA train pressure QLoRA train pressure (order)
7–8B ~14–16 GB Fits 24GB with care Comfortable on 12–24GB
13B ~26 GB Tight on 24GB Fits mid-range GPUs
34B ~68 GB Multi-GPU Often 1× 24–48GB-class
70B ~140 GB Multi-GPU heavy Often 1× 48–80GB-class

LoRA solved optimizer size; QLoRA solves weight storage during training. It does not invent labels, fix eval leakage, or make bitsandbytes optional on CUDA-less laptops.

A second problem is access inequality inside companies. Central ML platforms with big clusters can always run BF16 LoRA. Product squads with a single workstation cannot. QLoRA lets those squads produce candidate adapters that the platform later re-trains or validates at higher precision if needed. That workflow — prototype in QLoRA, confirm in BF16 LoRA when metrics are close — is healthier than blocking all experimentation until cluster quota appears.

What QLoRA does not solve: licensing of base weights, data rights, PII scrubbing, or the need for a prompt baseline. Those gates are identical to any fine-tuning project.

How We Got Here

Diagram: Memory-efficient fine-tuning path

timeline
    title From LoRA to QLoRA stacks
    2021 : LoRA
         : Low-rank adapters
    2022 : 8-bit optimizers
         : bitsandbytes momentum
    2023 : QLoRA paper
         : NF4 + double quant + paged Adam
    2023-2024 : HF PEFT defaults
              : k-bit prep helpers
    2025-2026 : Everyday 70B SFT
              : Unsloth/Axolotl wrappers

Once LoRA was standard, quantization of the frozen base became the next memory lever.

LLM.int8() and related work showed large models tolerate low-bit storage for inference. QLoRA specialized that insight for training: keep (W) quantized and frozen; compute in BF16; train LoRA. PEFT integrated the pattern so SFT and DPO recipes differ mainly by BitsAndBytesConfig.

What Is QLoRA?

QLoRA is LoRA on a 4-bit quantized frozen backbone:

  1. NF4 (4-bit NormalFloat) — quantization bins matched to roughly normal weight distributions (better information use than naive uniform INT4 for many nets).
  2. Double quantization — quantize the quantization constants themselves (commonly to 8-bit) to save ~0.37 bits/param-scale overhead.
  3. Paged optimizers — unified memory paging of optimizer state under spike pressure (gradient checkpointing, long sequences).

Adapters remain BF16/FP16. The 4-bit representation is a training storage format for (W), not a requirement for how you later serve the adapter.

In the Hugging Face stack, QLoRA is not a separate trainer class so much as a composition: BitsAndBytesConfig + prepare_model_for_kbit_training + LoraConfig via PEFT. That composition is why missing one flag looks like “QLoRA is broken” when the real issue is an incomplete recipe. Treat the three pieces as a unit in code review checklists.

How QLoRA Works

Forward pass

Diagram: Dequant + LoRA path

flowchart TB
  W4[NF4 frozen W] --> DQ[Dequantize to BF16]
  X[Input x] --> DQ
  DQ --> MUL[W_bf16 x]
  X --> A[LoRA A]
  A --> B[LoRA B]
  MUL --> SUM[Sum]
  B --> SUM
  SUM --> H[Hidden out]

Only A/B receive gradients; dequantized W is treated as a constant for autograd purposes.

Dequantization happens on the fly for the matmul. Implementations fuse operations where possible, but you still pay memory traffic that BF16 LoRA avoids. That is the root of the “QLoRA is not a speedup” rule. Gradient checkpointing further trades compute for activation memory — common and recommended on 70B-class single-GPU runs.

NF4 vs uniform INT4

Uniform INT4 wastes levels in the tails and under-resolves the dense center of typical weight histograms. NF4 places bins at Normal quantiles so more codes sit where mass is. That is why QLoRA standardized on bnb_4bit_quant_type="nf4". Block-wise quantization limits how far an outlier can poison a large tensor slice; still, a few pathological layers can absorb more error — another reason to keep a BF16 LoRA reference when stakes are high.

Double quantization

Each block of weights carries scale factors. Quantizing those scales shrinks auxiliary storage — small per parameter, material at 65B–70B. Enable with bnb_4bit_use_double_quant=True unless a vendor recipe documents a reason to disable it. The savings are not a quality feature; they are a packing feature.

Paged AdamW

optim="paged_adamw_8bit" (Transformers + bitsandbytes) reduces optimizer footprint and pages to CPU RAM when GPU memory spikes, trading occasional transfer latency for fewer OOMs. Spikes often appear at first steps, on long sequences, or when evaluation batches briefly inflate usage. If you see thrashing (steps suddenly 10× slower), shorten sequence length, reduce eval batch size, or add another GPU rather than disabling paging blindly.

Numerical caveats for practitioners

Layer norms and some heads may be kept in higher precision during k-bit prep for stability. Do not manually cast the entire module tree to 4-bit outside the supported APIs. When exporting, remember the adapter expects the same architecture as the BF16 base you will reload for serving; the NF4 train-time storage is not something you must ship to production hosts.

Architecture

Component Library Role
4-bit storage / dequant bitsandbytes NF4 weights
Adapters PEFT LoRA Trainable ΔW
k-bit prep prepare_model_for_kbit_training Checkpointing + norm casting
Loop TRL / Transformers SFT, DPO, etc.

Critical config:

from transformers import BitsAndBytesConfig
import torch

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

Diagram: Train vs serve precision choices

sequenceDiagram
  participant T as Trainer
  participant G as GPU
  participant S as Serving
  T->>G: Load base NF4 + LoRA BF16
  T->>G: Train adapters only
  T->>S: Export adapter artifact
  alt Quality-first serve
    S->>S: Load base BF16 + adapter (merge optional)
  else Cost-first serve
    S->>S: Merge then GPTQ/AWQ (separate eval)
  end

4-bit training and 4-bit inference are separate product decisions.

Step-by-Step Flow

  1. Install CUDA-enabled bitsandbytes, peft, transformers, trl, accelerate.
  2. Build BitsAndBytesConfig (NF4, double quant, BF16 compute).
  3. from_pretrained(..., quantization_config=bnb_config) — load directly in 4-bit.
  4. prepare_model_for_kbit_training(model).
  5. Attach LoraConfig (same ranks/targets as BF16 LoRA).
  6. Train with gradient checkpointing + paged AdamW 8-bit.
  7. Save adapter only.
  8. For serve: typically reload BF16/FP16 base + adapter; evaluate before any inference quant.

Diagram: Decision — LoRA vs QLoRA

flowchart TD
  A[Need PEFT SFT?] -->|No| P[Prompt / RAG]
  A -->|Yes| B{BF16 base fits?}
  B -->|Yes| L[LoRA BF16]
  B -->|No| C{NVIDIA CUDA?}
  C -->|No| S[Smaller model or cloud GPU]
  C -->|Yes| Q[QLoRA]
  L --> E[Eval gates]
  Q --> E
  E -->|Fail quality| F[More data / bigger r / full FT]
  E -->|Pass| D[Deploy adapter]

QLoRA is the memory fork of LoRA, not a different product category.

Real Production Example

Fine-tune Llama 3.1 70B Instruct style SFT on one A100 80GB. The same pattern applies to Mistral instruct checkpoints when licenses and chat templates match your deployment standards. Keep a BF16 LoRA run on an 8B proxy first to validate data formatting — catching template bugs is cheaper than discovering them after fourteen GPU-hours on 70B.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TrainingArguments
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training, PeftModel
from trl import SFTTrainer

MODEL_ID = "meta-llama/Llama-3.1-70B-Instruct"

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

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    quantization_config=bnb_config,
    device_map="auto",
    attn_implementation="flash_attention_2",
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
tokenizer.pad_token = tokenizer.eos_token

model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)
model = get_peft_model(
    model,
    LoraConfig(
        r=16,
        lora_alpha=32,
        target_modules=[
            "q_proj", "k_proj", "v_proj", "o_proj",
            "gate_proj", "up_proj", "down_proj",
        ],
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM",
    ),
)
model.print_trainable_parameters()

args = TrainingArguments(
    output_dir="./llama70b-qlora",
    num_train_epochs=1,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=16,
    learning_rate=2e-4,
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    bf16=True,
    optim="paged_adamw_8bit",
    gradient_checkpointing=True,
    max_grad_norm=0.3,
    eval_strategy="steps",
    eval_steps=50,
)

trainer = SFTTrainer(
    model=model,
    args=args,
    train_dataset=dataset["train"],
    eval_dataset=dataset["test"],
    processing_class=tokenizer,
)
trainer.train()
model.save_pretrained("./llama70b-qlora/adapter")

# Serve path: higher-precision base + adapter
base = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=torch.bfloat16, device_map="auto")
infer = PeftModel.from_pretrained(base, "./llama70b-qlora/adapter")

Peak train memory ~40–45GB-class with checkpointing; thousands of examples take many hours — budget wall-clock separately from VRAM success. For 7–8B on RTX 4090 24GB, the same recipe with larger microbatches is routine.

Design Decisions

Decision Prefer Notes
Quant type NF4 QLoRA default
Compute dtype BF16 on Ampere+ FP16 on older GPUs
Optimizer paged_adamw_8bit Fewer OOMs
Rank/targets Same as LoRA Memory win is from base quant
Serve dtype BF16 base + adapter Re-eval if you GPTQ/AWQ after merge
Alt stack Unsloth Faster kernels; still validate adapters
Sequence length Match product P95 Longer trains need more activation RAM
Gradient accum Keep effective batch stable Microbatch 1 is fine if accum compensates

When VRAM is still tight after QLoRA defaults, shorten sequences before you gut rank. Rank controls adapter capacity; sequence length often dominates peak memory via activations. Also prefer gradient checkpointing on before reducing dataset diversity — starving the model of examples to save memory is usually the worse trade.

Comparisons

Dimension LoRA (BF16 base) QLoRA (NF4 base)
Frozen W storage 16-bit 4-bit
Adapter precision BF16/FP16 BF16/FP16
VRAM Higher Lower
Step speed Often faster Dequant overhead
Dependency PEFT PEFT + bitsandbytes CUDA
Quality Reference Usually within ~1–2% task metrics — measure
Dimension QLoRA train quant GPTQ/AWQ serve quant
Purpose Fit training Fit/cheap inference
When applied from_pretrained train After merge / export
Eval need Vs BF16 LoRA Vs BF16 merged
Approach Use
Prompt / RAG First; knowledge & policy
LoRA Default when VRAM OK
QLoRA VRAM-bound open SFT
Full FT Proven PEFT ceiling + budget

Common Mistakes

  1. Loading BF16 then "quantizing later" — pass quantization_config at load.
  2. Skipping prepare_model_for_kbit_training.
  3. FP16 compute on BF16-native GPUs without reason.
  4. Non-paged AdamW on tight VRAM.
  5. Expecting speedups vs LoRA.
  6. Serving 4-bit train checkpoint without quality tests.
  7. Running on MPS/CPU expecting bitsandbytes 4-bit parity.
  8. Changing LoRA hyperparameters wildly "because QLoRA" — start from LoRA defaults.
  9. No comparison run on a smaller proxy vs BF16 LoRA.
  10. Treating job success as release — still need fine-tuning gates.

Where It Breaks Down

Hardware. Classic stack is NVIDIA CUDA. Apple Silicon and some AMD setups need different quantization stories.

Long context. Activations dominate; QLoRA may still OOM at 8K+ on 70B without parallel tricks.

Precision-sensitive tasks. Exact finance arithmetic, strict compilers, or regulated medical wording may show larger gaps — require side-by-side eval and human review.

Huge MoE / 400B+. Even 4-bit weight storage can demand multi-GPU; tooling is less cookie-cutter.

Throughput-oriented training farms. If you already have 8× GPUs, BF16 LoRA may finish sooner with simpler deps.

Silent config drift. A training image bumps bitsandbytes and suddenly loss curves change. Without pinned digests, teams attribute the shift to data. Containerize the train job and record digests in experiment tracking as first-class artifacts.

Eval on the wrong artifact. Measuring quality on the in-trainer 4-bit+adapter path, then serving BF16+adapter (or GPTQ merged), compares unequal systems. Always evaluate the promotion candidate binary that will face users.

Over-trusting paper averages. QLoRA’s original benchmarks showed strong parity on standard suites. Your internal taxonomy, tool grammar, or bilingual support corpus may sit in a different sensitivity regime. Papers authorize the method; they do not waive your golden set.

When NOT to Use QLoRA

Do not choose QLoRA when:

  • BF16 LoRA already fits and calendars care about step time;
  • the domain is extreme quality-sensitive and you lack a BF16 LoRA (or stronger) baseline plus frozen golden sets;
  • you only need prompting or RAG;
  • CUDA/bitsandbytes is unavailable and a smaller dense model would do;
  • stakeholders confuse "trained in 4-bit" with "must serve in 4-bit" without measurement;
  • your compliance process cannot pin CUDA/bitsandbytes digests for audit replay.

Honest scope: QLoRA is a memory technology for adapter training, not a guarantee of best possible quality. If leadership wants “70B quality on a laptop,” push back with measured proxies — often an 8B BF16 LoRA plus retrieval beats an under-evaluated 70B QLoRA on real tickets.

Running in Production

Dimension Practice
Pin bitsandbytes, PEFT, transformers, CUDA, base revision
Export Adapter + config; document NF4 train story in model card
Serve Prefer BF16/FP16 base + adapter; quantize inference separately
Eval Task + safety + optional BF16 LoRA delta report
Cost Fewer GPUs × longer wall time — model total $ , not GPU count alone
Prefs DPO/RLHF also use QLoRA when policies are huge

Important

A green training job only proves optimization ran. Promotion still requires independent quality, safety, latency, and cost gates on the served artifact.

Quality-sensitive release bar

For regulated or high-cost-of-error domains (clinical drafting support, legal clause assist, financial advice-adjacent tools), require an explicit comparison packet:

  • BF16 LoRA (or full FT) vs QLoRA on the same data split and seed budget where affordable;
  • slice metrics for rare classes, long inputs, and multilingual traffic;
  • human review on a stratified sample, not only automated scores;
  • memorization / extraction probes if training data included sensitive strings.

If you cannot fund that packet, do not market QLoRA as “equal quality.” Ship a smaller BF16 model, a stronger prompted baseline, or a hosted fine-tune with clearer eval ownership instead.

Cost modeling example

Suppose 70B BF16 LoRA needs 8×A100 for two hours ($16/hr class hardware → $256) while QLoRA fits 1×A100 for fourteen hours ($28). QLoRA wins on cash even though it is slower per step. Invert the story when a multi-GPU cluster is already idle and engineer time dominates — waiting overnight for QLoRA may cost more in calendar risk than the GPU invoice savings. Always multiply dollars × iteration latency × failure probability.

Dependency and reproducibility traps

Pin bitsandbytes builds to the CUDA version in the training image. Divergent wheels are a common “works on my box” failure. Store the exact BitsAndBytesConfig JSON beside the adapter. When auditors ask how a model was trained, “we used QLoRA” is incomplete — NF4, double quant, rank, targets, and base revision are the minimal reproducible set.

Continue Learning

Production Checklist

  • bitsandbytes version pinned to training CUDA image
  • NF4 configuration verified and stored with adapter
  • BF16 compute dtype validated on target GPUs
  • Base model and tokenizer revisions pinned
  • Memory / VRAM benchmark completed for train and serve paths
  • Adapter + BitsAndBytesConfig exported as release artifacts
  • Served artifact evaluated (not only in-loop trainer metrics)
  • Optional BF16 LoRA quality delta reported for high-stakes domains
  • Cost model includes wall time × GPU rate (not GPU count alone)
  • Inference quantization treated as a separate stage gate
  • Rollback adapter and prior config retained

Prerequisites

Core Concepts

Advanced Topics

Diagram: QLoRA among PEFT methods

flowchart LR
    FT[Fine-tuning] --> PEFT[PEFT]
    PEFT --> LoRA[LoRA]
    LoRA --> QLoRA[QLoRA]
    PEFT --> DPO[DPO]
    PEFT --> RLHF[RLHF]
    FT --> LLM[LLMs]
    LLM --> TR[Transformers]

Interview Questions

What three techniques define QLoRA?

NF4 4-bit storage for frozen weights, double quantization of constants, and paged optimizers — plus LoRA adapters in higher precision.

Why can quality remain close to BF16 LoRA?

Because the trainable parameters are the adapters; quantization noise sits on frozen (W). Residual gaps still need measurement.

Is QLoRA faster than LoRA?

Usually not. It trades compute/dequant overhead for VRAM headroom.

Train in 4-bit, serve in 4-bit?

Not required. Common pattern: train QLoRA, serve BF16 base + adapter, optionally apply GPTQ/AWQ after merge with a new eval.

What fails without prepare_model_for_kbit_training?

Unstable norms / broken checkpointing patterns — training that OOMs or diverges mysteriously.

When would you refuse QLoRA in a design review?

Quality-critical domain without side-by-side eval capacity, or when BF16 LoRA already fits and simplicity matters more than GPU count.

Key Takeaways

  • QLoRA stores frozen bases in NF4 and trains LoRA adapters in high precision.
  • It is the VRAM lever on top of LoRA inside PEFT — not a free quality or speed upgrade.
  • bitsandbytes flags and k-bit prep are part of the contract; pin versions.
  • Separate training quantization from serving quantization; evaluate both.
  • Decline QLoRA when you cannot measure risk in sensitive domains.
  • Prototype on QLoRA when GPUs are scarce, then confirm with BF16 LoRA (or stronger) whenever the product’s cost of error is high.
  • Wall-clock time, engineer waiting time, and GPU invoice are different currencies — optimize the one that actually constrains your roadmap.
  • Keep the PEFT family linked in docs and runbooks: fine-tuningPEFTLoRA → QLoRA for memory-bound open-weight training.

FAQs

Memory needed?

Rough orders: 7B ~6–10GB; 70B ~40–48GB+ with checkpointing — measure on your seq len and batch.

NF4 vs FP4?

NF4 is the usual QLoRA choice for LLM weights; do not switch casually.

Apple Silicon?

Use BF16/FP16 LoRA or remote CUDA; do not assume bitsandbytes 4-bit parity on MPS.

Flash Attention?

Yes — pass attn_implementation="flash_attention_2" when available to save activation memory.

Learning rate?

Same ballpark as LoRA (e.g. 2e-4 start).

GPTQ base + QLoRA?

Prefer quantizing from the official BF16/FP16 checkpoint with bitsandbytes NF4 for training. Pre-GPTQ bases are a different path.

Unsloth?

Optimized QLoRA-style training; still run your eval harness on exported adapters.

Can I resume interrupted QLoRA jobs?

Yes if you save PEFT checkpoints and optimizer state consistently. Confirm that resumed runs reload the same BitsAndBytesConfig. Mismatched quant flags after resume are a subtle corruption source.

Does double quantization hurt quality?

In the original QLoRA results and common practice, the memory win dominates and task metrics stay close — but your domain may differ. If you have VRAM headroom, A/B with bnb_4bit_use_double_quant on vs off once; do not perpetual-tweak mid-campaign.

References

Further Reading

Next Topics

Learning Path

Continue Learning

Related Guides

Related Tools

ToolCategoryPurposeWebsiteBest For
Hugging Face Transformers
Python SDK
frameworksLibrary for using pretrained transformers in Python and beyond.huggingface.coModel fine-tuning
Llama
Python SDK
ai productsOpen model family from Meta for local and hosted LLMs.ai.meta.comSelf-hosted AI
Ollama
Open SourceAPI
servingLocal model runner with simple command and HTTP interface.ollama.aiLocal LLM development