QLoRA 4-bit Fine-Tuning Tutorial: Single GPU, 7B to 70B
QLoRA 4-bit Fine-Tuning Tutorial: Single GPU, 7B to 70B
Photo by AltumCode on Unsplash
Quick Answer: QLoRA (Quantized Low-Rank Adaptation) lets you fine-tune models up to 70B parameters on a single consumer GPU by keeping the base model in 4-bit and training only small adapter weights. A 7B model needs ~7GB VRAM, 13B needs ~10GB, 34B needs ~16GB, and 70B needs ~24-32GB VRAM. This tutorial covers the complete setup: environment, dataset preparation, configuration, training loop, saving, and inference.
What Is QLoRA and Why Use It?
QLoRA combines two techniques:
- Quantization (Q): The base model is stored in 4-bit precision (NF4), reducing memory from 2 bytes per parameter to 0.5 bytes
- LoRA: Small, low-rank adapter matrices are trained in full precision while the base model stays frozen
| Technique | Base Model Precision | Trainable Parameters | Memory for 70B | Quality |
|---|---|---|---|---|
| Full fine-tune (FP16) | FP16 | All 70B | 700+ GB | Best |
| LoRA (FP16) | FP16 | ~0.1-1% | 160 GB | Near-best |
| QLoRA (NF4) | NF4 (4-bit) | ~0.1-1% | 35-48 GB | Near-LoRA |
"QLoRA is the most efficient way to adapt large models when you don't have a GPU cluster. The quality gap with full fine-tuning is minimal for most tasks, especially with rank >= 64." — Tim Dettmers, QLoRA paper author
Hardware Requirements by Model Size
Single GPU — Inference-Grade Fine-Tuning
| Model Size | Min VRAM | Recommended VRAM | GPU Options |
|---|---|---|---|
| 1B-3B | 4 GB | 6 GB | RTX 3060 12GB, RTX 4060 |
| 7B-8B | 8 GB | 12 GB | RTX 3060 12GB, RTX 4060 Ti 16GB |
| 13B | 12 GB | 16 GB | RTX 4070 Ti Super 16GB |
| 34B | 16 GB | 24 GB | RTX 4090 24GB |
| 70B | 28 GB | 32 GB | RTX 5090 32GB, A6000 48GB |
| 120B | 48 GB | 80 GB | A100 80GB, H100 80GB |
Key Memory Consumers in QLoRA
Model weights (NF4): 0.5 bytes × 70B = 35 GB
LoRA adapters (FP16): 2 bytes × rank × layers (~200MB for rank=16)
Gradients: 2 bytes × rank × layers (~200MB)
Optimizer states (Adam): 8 bytes × rank × layers (~800MB for rank=16)
Activations + buffer: 2-8 GB (batch_size dependent)
────────────────────────────────────────────────────
Total: ~38-44 GB for 70B
Environment Setup
Install Dependencies
# Python 3.10+ required
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
pip install transformers datasets accelerate peft trl bitsandbytes
pip install wandb # optional: experiment tracking
pip install xformers # optional: memory-efficient attention
Verify GPU Setup
import torch
import bitsandbytes as bnb
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
print(f"BitsAndBytes version: {bnb.__version__}")
Dataset Preparation
Format Your Data
QLoRA works best with a structured chat format. Here's the recommended format for instruction tuning:
[
{
"instruction": "Explain the difference between LoRA and QLoRA",
"input": "",
"output": "LoRA adds trainable low-rank matrices to attention layers..."
},
{
"instruction": "Write a Python function for matrix multiplication",
"input": "The matrices are 3x3",
"output": "```python
def multiply_3x3(A, B):
...```"
}
]
Load and Tokenize
from datasets import load_dataset
dataset = load_dataset("json", data_files="training_data.json")
def formatting_func(example):
text = f"### Instruction: {example['instruction']}
"
if example['input']:
text += f"### Input: {example['input']}
"
text += f"### Response: {example['output']}"
return {"text": text}
dataset = dataset.map(formatting_func)
# Tokenize
def tokenize(element):
return tokenizer(
element["text"],
truncation=True,
max_length=2048,
padding="max_length"
)
dataset = dataset.map(tokenize, remove_columns=["text", "instruction", "input", "output"])
Model Loading with 4-bit Quantization
This is the core of QLoRA — loading the base model in NF4 (4-bit NormalFloat):
import torch
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
BitsAndBytesConfig
)
model_name = "meta-llama/Llama-3.1-8B" # or "meta-llama/Llama-3.1-70B"
# 4-bit quantization config
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NF4 is QLoRA's recommended type
bnb_4bit_compute_dtype=torch.bfloat16, # Compute in BF16 for stability
bnb_4bit_use_double_quant=True, # Double quantization for extra savings
)
# Load tokenizer and model
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto", # Auto-distribute across GPU/CPU
torch_dtype=torch.bfloat16,
trust_remote_code=True,
)
Photo by Ismael Campos Carrillo on Pexels
LoRA Configuration
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
# Prepare model for k-bit training (freeze base, enable gradients for LoRA)
model = prepare_model_for_kbit_training(model)
# LoRA configuration
lora_config = LoraConfig(
r=16, # Rank — higher = more expressive but more memory
lora_alpha=32, # Scaling factor (typically 2x rank)
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"], # All linear layers
lora_dropout=0.05, # Dropout for regularization
bias="none", # Don't train bias terms
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Output: trainable params: 8,388,608 || all params: 7,000,000,000 || trainable: 0.12%
Choosing the Right Rank
| Rank | Trainable Params | Memory Overhead | Quality | Best For |
|---|---|---|---|---|
| r=8 | ~0.06% | Minimal | Adequate | Simple task adaptation |
| r=16 | ~0.12% | Low | Good | Default — most tasks |
| r=32 | ~0.24% | Moderate | Better | Complex reasoning tasks |
| r=64 | ~0.48% | Higher | Best | Domain-specific expertise |
| r=128 | ~0.96% | High | Marginal gains | Very large datasets |
Training Configuration
from transformers import TrainingArguments
from trl import SFTTrainer
# Training arguments optimized for single GPU
training_args = TrainingArguments(
output_dir="./qlora-output",
per_device_train_batch_size=1, # Small batch for large models
gradient_accumulation_steps=4, # Accumulate to simulate batch_size=4
gradient_checkpointing=True, # Memory savings (trade: slower)
num_train_epochs=3, # Adjust based on dataset size
learning_rate=2e-4, # Higher than full fine-tune
fp16=True, # Mixed precision training
logging_steps=10,
save_steps=100,
save_total_limit=2,
optim="paged_adamw_8bit", # 8-bit optimizer saves VRAM
lr_scheduler_type="cosine",
warmup_ratio=0.03,
max_grad_norm=0.3,
report_to="none", # or "wandb"
)
# Use SFTTrainer for supervised fine-tuning
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
tokenizer=tokenizer,
dataset_text_field="text",
max_seq_length=2048,
packing=True, # Pack multiple short sequences
)
Running the Training Loop
# Start training
trainer.train()
# If you want to resume from a checkpoint:
# trainer.train(resume_from_checkpoint=True)
Monitoring Training
Watch these metrics in your training logs:
Step Training Loss VRAM Used Tokens/sec
10 1.85 11.2 GB 420
50 1.42 11.2 GB 415
100 1.18 11.2 GB 418
200 0.95 11.2 GB 422
Warning signs:
- VRAM approaching 100% → reduce batch size or max_seq_length
- Loss not decreasing → check learning rate or dataset quality
- Loss = 0.0 → model is memorizing (overfitting), add dropout or reduce epochs
Saving and Loading the Adapter
Save the LoRA Adapter (NOT the full model)
# Save only the LoRA weights (tiny, ~10-50MB)
model.save_pretrained("./my-finetuned-adapter")
tokenizer.save_pretrained("./my-finetuned-adapter")
# Optional: merge with base model for faster inference
from peft import PeftModel
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./my-merged-model")
Load for Inference
# Option 1: Load base + adapter separately
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B",
quantization_config=bnb_config,
device_map="auto"
)
model = PeftModel.from_pretrained(base_model, "./my-finetuned-adapter")
# Option 2: Load merged model
model = AutoModelForCausalLM.from_pretrained("./my-merged-model", device_map="auto")
Inference with the Fine-Tuned Model
def generate_response(prompt, max_length=512):
messages = [{"role": "user", "content": prompt}]
input_ids = tokenizer.apply_chat_template(
messages,
return_tensors="pt",
add_generation_prompt=True
).to("cuda")
outputs = model.generate(
input_ids,
max_new_tokens=max_length,
temperature=0.7,
top_p=0.9,
do_sample=True,
)
response = tokenizer.decode(outputs[0][input_ids.shape[1]:], skip_special_tokens=True)
return response
# Test it
print(generate_response("Explain quantum computing in simple terms"))
Common Errors and Solutions
| Error | Cause | Solution |
|---|---|---|
| CUDA out of memory | Model too large for VRAM | Reduce batch size, max_seq_length, or use smaller rank |
bitsandbytes not compatible | Wrong CUDA version | Reinstall: pip install bitsandbytes --force-reinstall |
| Loss is NaN | Learning rate too high | Reduce lr to 1e-4 or 5e-5 |
| Training very slow | No xformers/flash-attn | Install: pip install flash-attn --no-build-isolation |
| Tokenizer missing pad_token | Some models don't set it | Explicitly set tokenizer.pad_token = tokenizer.eos_token |
Related Reads
- Fine-Tune Llama 3.1 8B with QLoRA on a Consumer GPU
- LoRA vs QLoRA: Fine-Tuning on Consumer GPUs Explained
- Llama 3.1 70B Hardware Requirements: GPU, VRAM, RAM Guide
Advanced Memory Optimization Techniques
Beyond the core QLoRA setup, several advanced techniques can further reduce VRAM usage or accelerate training without sacrificing quality. FlashAttention-2 is a drop-in replacement for standard attention that reduces memory overhead by computing attention in blocks and avoiding materialization of large intermediate matrices. Install it via pip install flash-attn --no-build-isolation and enable it by passing attn_implementation="flash_attention_2" to from_pretrained(). This can cut activation memory by 20-30% for long sequences (e.g., 2048 tokens), allowing larger batch sizes or higher ranks.
For models exceeding 34B parameters, CPU offloading can help bridge the gap between GPU VRAM and model size. Use device_map="auto" with accelerate to automatically offload layers to CPU when GPU memory is exhausted. While this slows training due to PCIe bottlenecks, it enables fine-tuning 70B models on GPUs with as little as 24GB VRAM. Combine this with max_memory in from_pretrained() to explicitly control offloading:
``python max_memory = {0: "24GB", "cpu": "64GB"} # 24GB GPU, 64GB RAM model = AutoModelForCausalLM.from_pretrained( model_name, quantization_config=bnb_config, device_map="auto", max_memory=max_memory ) ``
Gradient checkpointing (enabled via gradient_checkpointing=True) is another critical tool, trading compute for memory by recomputing activations during the backward pass instead of storing them. For QLoRA, this can reduce memory usage by 30-40% at the cost of ~20% slower training. Pair it with gradient_accumulation_steps to simulate larger batch sizes without increasing VRAM.
Dataset Curation and Quality Control
The quality of your fine-tuning dataset often matters more than its size. For instruction tuning, prioritize diverse, high-quality examples over sheer volume—1,000 well-curated samples can outperform 10,000 noisy ones. Use these strategies to improve dataset efficacy:
- Deduplication: Remove near-identical examples (e.g., using MinHash or exact string matching) to prevent overfitting and reduce training time. Tools like
datasets’deduplicatemethod ordatasketchcan automate this. - Difficulty balancing: Include a mix of simple and complex instructions to prevent the model from over-optimizing for easy tasks. Use perplexity or human evaluation to filter out ambiguous or low-quality examples.
- Domain-specific augmentation: For specialized tasks (e.g., coding, legal), generate synthetic examples using templates or weaker models, then validate them with domain experts or automated tests.
For multi-turn conversations, structure your dataset to simulate realistic dialogue flows. Use a format like:
``json { "conversations": [ {"role": "user", "content": "Explain QLoRA in one sentence."}, {"role": "assistant", "content": "QLoRA fine-tunes large models on consumer GPUs by quantizing the base model to 4-bit and training small adapter weights."}, {"role": "user", "content": "What’s the memory savings?"}, {"role": "assistant", "content": "~4x compared to FP16, e.g., 70B fits in ~40GB VRAM."} ] } ``
Tokenize multi-turn data with tokenizer.apply_chat_template() to ensure proper role separation and special tokens. For long sequences, use truncation=True and max_length to avoid OOM errors, but ensure critical context isn’t lost—consider splitting long conversations into chunks with overlapping context.
Post-Training Evaluation and Iteration
After training, evaluate your model rigorously to identify strengths and weaknesses before deployment. Start with automated metrics like perplexity (for language modeling) or task-specific scores (e.g., ROUGE for summarization, pass@k for coding). For instruction-tuned models, use human evaluation or LLM-as-a-judge frameworks to assess response quality, coherence, and adherence to instructions. Tools like lm-evaluation-harness or FastChat can streamline this process.
Key evaluation dimensions include:
- Faithfulness: Does the model follow instructions precisely, or does it hallucinate or deviate?
- Safety: Does it avoid harmful, biased, or toxic outputs? Use benchmarks like
ToxiGenorBBQto test this. - Robustness: How well does it handle edge cases, typos, or ambiguous inputs?
- Efficiency: Measure inference latency and VRAM usage, especially if deploying on edge devices.
For iterative improvement, ablation studies help isolate the impact of hyperparameters. Test variations in LoRA rank (e.g., 16 vs. 64), learning rate (e.g., 2e-4 vs. 1e-4), or dataset size (e.g., 1k vs. 10k examples) while holding other variables constant. Use WandB or TensorBoard to track experiments and compare results visually.
If the model underperforms, diagnose the issue:
- High training loss but low eval loss: Likely overfitting—reduce epochs, add dropout, or increase dataset diversity.
- Low training and eval loss but poor outputs: The model may be memorizing—check for dataset leakage or insufficient instruction diversity.
- High eval loss but good outputs: The evaluation metric may not align with your task (e.g., perplexity vs. human judgment).
Finally, consider quantizing the merged model for deployment. Use bitsandbytes’s nn.Linear4bit or GGUF to convert the model to 4-bit or 8-bit precision, reducing inference memory by 2-4x with minimal quality loss. For example:
``python from bitsandbytes.nn import Linear4bit model = AutoModelForCausalLM.from_pretrained("./my-merged-model", load_in_4bit=True) ``
Key Takeaways
- QLoRA enables fine-tuning models up to 70B parameters on a single consumer GPU by quantizing the base model to 4-bit (NF4) while training only small LoRA adapter weights, reducing VRAM needs to ~24-32GB for 70B models.
- Use
BitsAndBytesConfigwithload_in_4bit=True,bnb_4bit_quant_type="nf4", andbnb_4bit_compute_dtype=torch.bfloat16to load models in 4-bit precision, cutting memory usage by ~4x compared to FP16. - Set LoRA rank (
r) between 16-64 for most tasks—higher ranks (e.g., 64) improve quality for complex reasoning but increase memory overhead (~0.5% trainable parameters per 64 rank for 70B models). - Optimize training for single-GPU setups with
gradient_accumulation_steps=4,gradient_checkpointing=True, andpaged_adamw_8bitto fit large models without OOM errors, even withper_device_train_batch_size=1. - Save only the LoRA adapter weights (~10-50MB) instead of the full model to preserve disk space; merge adapters with the base model post-training for faster inference if needed.
- Monitor VRAM usage and training loss closely—spikes in VRAM or stagnant loss indicate overfitting, requiring adjustments to batch size, learning rate (
2e-4is a safe starting point), or dataset quality.
Frequently Asked Questions
What's the difference between QLoRA and regular LoRA?
QLoRA quantizes the base model to 4-bit, reducing memory by 4x compared to LoRA (which keeps the base in FP16). The LoRA adapters themselves are trained in FP16 in both cases. QLoRA trades ~1-2% quality for ~4x memory savings.
Can I fine-tune a 70B model on an RTX 5090 (32GB)?
Yes, but it's tight. 70B at NF4 is ~35GB for weights. With LoRA adapters, gradients, and optimizer states, you need ~38-44GB. You can make it fit by enabling gradient checkpointing, using paged AdamW 8-bit, and setting gradient_accumulation_steps > 1 with batch_size=1.
How long does QLoRA training take?
For a 7B model with 1,000 examples, rank=16, 3 epochs:
Does QLoRA work with vision models?
QLoRA works best with text-only LLMs. For vision-language models (LLaVA, Qwen-VL), use LoRA with the language component only. Vision encoders are typically frozen during fine-tuning.
Can I use QLoRA with any HuggingFace model?
Any model that supports transformers and has linear layers for attention works. Models from Llama, Mistral, Qwen, DeepSeek, and Phi families are all supported. The target_modules list may need adjustment for different architectures.


Comments
Sign in to join the conversation
No comments yet. Be the first to share your thoughts!