Fine-Tune Llama 3.1 8B with QLoRA on a Consumer GPU

Fine-Tune Llama 3.1 8B with QLoRA on a Consumer GPU
Photo by DΛVΞ GΛRCIΛ on Pexels
Quick Answer: You can fine-tune Llama 3.1 8B on any GPU with 8GB+ VRAM using QLoRA. With a RTX 3090/4090/5090 (24GB+), you can fine-tune at 4-bit quantization, batch size 4-8, sequence length 2048, completing most datasets in 1-8 hours. The key tools:
transformers,peft,bitsandbytes, andtrl(TRL) from Hugging Face. Expected cost: $0 (your own GPU) or ~$2-5 on RunPod/Vast.ai for a full training run. This tutorial covers dataset preparation, LoRA config, training, saving, and deployment.
What You Need (Hardware + Software)
Minimum Hardware
| GPU | VRAM | Works? | Batch Size | Training Time (1K samples) |
|---|---|---|---|---|
| RTX 3060 | 12 GB | ✅ | 2-4 | ~4-6 hours |
| RTX 3090 | 24 GB | ✅ | 4-8 | ~2-3 hours |
| RTX 4090 | 24 GB | ✅ | 4-8 | ~1.5-2.5 hours |
| RTX 5090 | 32 GB | ✅ | 8-16 | ~1-2 hours |
| M4 Max | Unified | ✅ | 2-4 | ~3-5 hours |
| Free Colab T4 | 16 GB | ⚠️ Limited | 1-2 | ~8-10 hours (may disconnect) |
Software Stack
Python 3.10+
PyTorch 2.4+ (CUDA 12.1+)
transformers 4.45+
peft 0.12+
bitsandbytes 0.44+
trl 0.10+
datasets 2.20+
accelerate 0.32+
Step 1: Install Dependencies
# Create environment
python -m venv llama-finetune
source llama-finetune/bin/activate # or .\venv\Scripts\activate on Windows
# Install core packages
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
pip install transformers peft bitsandbytes trl accelerate datasets
pip install wandb # optional: for experiment tracking
pip install xformers # optional: memory-efficient attention
Verify GPU Access
import torch
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")
Step 2: Prepare Your Dataset
Format: Conversational (ChatML)
The recommended format for Llama 3.1 is the ChatML format:
{
"messages": [
{"role": "system", "content": "You are a helpful assistant specialized in Python programming."},
{"role": "user", "content": "Write a Python function to reverse a linked list."},
{"role": "assistant", "content": "Here's a Python function to reverse a linked list..."}
]
}
Load Your Dataset
from datasets import load_dataset
# Option 1: Load from Hugging Face Hub
dataset = load_dataset("your-username/your-dataset", split="train")
# Option 2: Load from local JSONL file
dataset = load_dataset("json", data_files="my_data.jsonl", split="train")
# Option 3: Create from list of dicts
dataset = load_dataset(
"json",
data_files=[{"train": "train.jsonl", "test": "test.jsonl"}]
)
# Format messages for the model
def format_chat(example):
"""Convert messages list to a single string with ChatML format."""
formatted = ""
for msg in example["messages"]:
if msg["role"] == "system":
formatted += f"<|begin_of_text|><|start_header_id|>system<|end_header_id|>
{msg['content']}<|eot_id|>
"
elif msg["role"] == "user":
formatted += f"<|start_header_id|>user<|end_header_id|>
{msg['content']}<|eot_id|>
"
elif msg["role"] == "assistant":
formatted += f"<|start_header_id|>assistant<|end_header_id|>
{msg['content']}<|eot_id|>
"
return {"text": formatted}
dataset = dataset.map(format_chat)
# Split into train/test
dataset = dataset.train_test_split(test_size=0.1, seed=42)
train_dataset = dataset["train"]
eval_dataset = dataset["test"]
print(f"Train: {len(train_dataset)} samples, Test: {len(eval_dataset)} samples")
Step 3: Load the Model in 4-bit
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
# 4-bit quantization config
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NormalFloat4 (optimal for 4-bit)
bnb_4bit_use_double_quant=True, # Double quantization (saves ~0.5 GB)
bnb_4bit_compute_dtype=torch.bfloat16, # Compute in bf16
)
# Load model
model_name = "meta-llama/Llama-3.1-8B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto", # Automatic GPU placement
trust_remote_code=True,
use_flash_attention_2=True, # Faster training, needs xformers
)
# Load tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right" # Important for Llama models
# Prepare model for k-bit training
model = prepare_model_for_kbit_training(model)
Memory Check After Loading
Model loaded in 4-bit: ~5.5 GB VRAM
With Python overhead: ~6.5 GB
Remaining for training: ~17.5 GB (on 24GB GPU)
Step 4: Configure LoRA
# LoRA configuration
lora_config = LoraConfig(
r=16, # Rank: 8-64 typical. Higher = more expressiveness, more memory
lora_alpha=32, # Scaling factor (often 2x rank)
target_modules=[ # Which modules to apply LoRA to
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
],
lora_dropout=0.05, # Regularization
bias="none",
task_type="CAUSAL_LM",
)
# Apply LoRA to the model
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Expected output: trainable params: ~42M / 8B = ~0.5% of total params
LoRA Rank Guide
| Rank | Trainable Params | Memory Overhead | Quality | Use Case |
|---|---|---|---|---|
| r=8 | ~21M | Low | Good | Small datasets, simple tasks |
| r=16 | ~42M | Medium | Better | Default — works for most |
| r=32 | ~84M | Higher | Best | Complex tasks, larger datasets |
| r=64 | ~168M | High | Marginal | Usually overkill for 8B |
Photo by Ravi Kant on Pexels
Step 5: Training Arguments and Run
# Training arguments
training_args = TrainingArguments(
output_dir="./llama-8b-qlora-output",
num_train_epochs=3, # Number of passes over data
per_device_train_batch_size=4, # Adjust based on VRAM
per_device_eval_batch_size=2,
gradient_accumulation_steps=4, # Effective batch = 4×4=16
gradient_checkpointing=True, # Saves VRAM at cost of speed
optim="paged_adamw_8bit", # 8-bit optimizer (saves VRAM)
logging_steps=10,
learning_rate=2e-4, # Typical for LoRA
weight_decay=0.001,
fp16=False,
bf16=True, # Use bf16 if supported (H100, 5090)
max_grad_norm=0.3,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
evaluation_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=100,
save_total_limit=3,
report_to="wandb", # Or "none" to disable
run_name="llama-8b-qlora-run-1",
)
# Initialize trainer
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
max_seq_length=2048, # Max context length for training
dataset_text_field="text", # Field containing formatted text
packing=False, # Packing = faster but less controlled
)
# Start training
trainer.train()
GPU Memory During Training
| Batch Size | Context Length | VRAM Usage | GPU Needed |
|---|---|---|---|
| 2 | 2048 | ~12 GB | RTX 3060 12GB |
| 4 | 2048 | ~16 GB | RTX 3090/4090 |
| 8 | 2048 | ~22 GB | RTX 5090 |
| 4 | 4096 | ~20 GB | RTX 4090/5090 |
| 8 | 4096 | ~30 GB | 2x GPU or RTX 5090 |
Step 6: Save and Merge
Save LoRA Adapter (Small, Fast)
# Save just the LoRA adapter weights (~42 MB)
model.save_pretrained("./llama-8b-qlora-adapter")
tokenizer.save_pretrained("./llama-8b-qlora-adapter")
Merge with Base Model (Full Model, Slow)
from peft import PeftModel
# Load base model in full precision for merge
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
torch_dtype=torch.bfloat16,
device_map="auto",
)
# Load LoRA adapter
merged_model = PeftModel.from_pretrained(base_model, "./llama-8b-qlora-adapter")
# Merge and unload
merged_model = merged_model.merge_and_unload()
# Save merged model (full ~16 GB for 8B FP16)
merged_model.save_pretrained("./llama-8b-merged")
tokenizer.save_pretrained("./llama-8b-merged")
Step 7: Deploy with Ollama
Convert to GGUF and Run Locally
# 1. Convert merged model to GGUF
pip install llama-cpp-python
python convert_hf_to_gguf.py ./llama-8b-merged \
--outfile ./llama-8b-merged-q4_k_m.gguf \
--outtype q4_k_m
# Or use llama.cpp's convert script
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
python convert.py ../llama-8b-merged
./quantize ../llama-8b-merged/ggml-model-f16.gguf \
../llama-8b-merged-q4_k_m.gguf q4_k_m
# 2. Create Ollama model
# Create Modelfile:
cat > Modelfile << EOF
FROM ./llama-8b-merged-q4_k_m.gguf
TEMPLATE """{{ .System }}
{{ .Prompt }}"""
PARAMETER temperature 0.7
PARAMETER top_p 0.9
EOF
# 3. Create and run
ollama create my-finetuned-model -f Modelfile
ollama run my-finetuned-model
Deploy with vLLM (Production)
# Serve the merged model with vLLM
python -m vllm.entrypoints.openai.api_server \
--model ./llama-8b-merged \
--dtype bfloat16 \
--max-model-len 4096 \
--gpu-memory-utilization 0.90
# Or serve the LoRA adapter on top of base model
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-8B-Instruct \
--enable-lora \
--lora-modules my-adapter=./llama-8b-qlora-adapter
Expected Training Times by GPU
1,000 Training Samples, 3 Epochs, Seq Length=2048
| GPU | Batch Size | Time to Train | Cost (Cloud) |
|---|---|---|---|
| RTX 3060 (12 GB) | 2 | ~5 hours | ~$2.50 on Vast.ai |
| RTX 3090 (24 GB) | 4 | ~2.5 hours | ~$1.50 on Vast.ai |
| RTX 4090 (24 GB) | 4 | ~2 hours | ~$2.00 on RunPod |
| RTX 5090 (32 GB) | 8 | ~1.5 hours | Owned |
| H100 | 16 | ~30 minutes | ~$1.50 on RunPod |
Scaling Up
| Dataset Size | Epochs | RTX 3090 | RTX 4090 | RTX 5090 |
|---|---|---|---|---|
| 500 samples | 3 | ~1 hour | ~45 min | ~30 min |
| 1,000 samples | 3 | ~2.5 hours | ~2 hours | ~1.5 hours |
| 5,000 samples | 3 | ~12 hours | ~9 hours | ~6 hours |
| 10,000 samples | 3 | ~24 hours | ~18 hours | ~12 hours |
Common Pitfalls and Solutions
| Problem | Cause | Solution |
|---|---|---|
| CUDA Out of Memory | Batch size too large | Reduce per_device_batch_size to 2 or 1, enable gradient_checkpointing |
| Loss goes to NaN | Learning rate too high | Reduce learning_rate to 1e-4 or 5e-5 |
| Model repeats same text | Overfitting on small dataset | Reduce epochs to 1-2, add weight decay |
| Training too slow | Unoptimized attention | Install xformers, enable flash_attention_2 |
| Tokenizer warnings | Padding side | Set tokenizer.padding_side="right" |
| LoRA not learning | Wrong target modules | Include all projection layers: q, k, v, o, gate, up, down |
Related Reads
- QLoRA 4-bit Fine-Tuning Tutorial: Single GPU, 7B to 70B
- LoRA vs QLoRA: Fine-Tuning on Consumer GPUs Explained
- Llama 3.1 70B Hardware Requirements: GPU, VRAM, RAM Guide
Key Takeaways
- Use QLoRA with 4-bit quantization to fine-tune Llama 3.1 8B on GPUs with 8GB+ VRAM—RTX 3090/4090/5090 enables batch sizes 4-8 at 2048 sequence length, completing 1K samples in 1.5-3 hours with
bitsandbytes,peft, andtrllibraries. - Format datasets in ChatML with
<|start_header_id|>and<|eot_id|>tokens (e.g.,{"messages": [{"role": "system", "content": "..."}, ...]}), then map to a singletextfield for training usingdataset.map(format_chat). - Configure LoRA with
r=16,lora_alpha=32, and target modulesq_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_projto balance memory (42M trainable params) and quality—higher ranks (e.g., 32) suit complex tasks but increase VRAM usage. - Set training arguments with
gradient_accumulation_steps=4,gradient_checkpointing=True,bf16=True, andoptim="paged_adamw_8bit"to optimize VRAM; useper_device_train_batch_size=4on 24GB GPUs for stable training at ~16GB VRAM usage. - Save the LoRA adapter (~42MB) for lightweight deployment or merge it with the base model (16GB FP16) for standalone use—convert to GGUF for Ollama or serve via vLLM with
--enable-lorafor multi-adapter support. - Troubleshoot common issues: reduce batch size/learning rate (e.g.,
2e-4) for OOM/NaN loss, enablexformersfor faster attention, and ensuretokenizer.padding_side="right"to avoid tokenizer warnings.
Frequently Asked Questions
Can I fine-tune Llama 3.1 8B on a 8GB GPU?
With QLoRA 4-bit and batch size 1, yes. You'll need gradient checkpointing and may need to use gradient accumulation of 4+ to maintain effective batch size. Training will be slow (1-2 samples/second) but it works.
How much data do I need for fine-tuning?
100-500 high-quality examples for noticeable improvement in a specific task. 1,000-5,000 for significant behavioral change. Quality matters more than quantity — 200 curated examples beats 10,000 noisy ones.
Should I merge the LoRA adapter or keep it separate?
Keep it separate for development (fast upload/download, low storage). Merge it for deployment (single model file, no dependency on adapter loading). Use vLLM's native LoRA support if you want to serve multiple adapters from one base model.
What's the difference between full fine-tune and LoRA?
Full fine-tune updates all 8B parameters (needs 16x more VRAM). LoRA updates ~42M parameters (0.5%). LoRA achieves 70-95% of full fine-tune quality depending on the task and dataset size. For consumer GPUs, use LoRA.
Can I fine-tune on cloud GPUs instead of buying one?
Yes — RunPod, Vast.ai, Lambda Labs, and Google Colab all have suitable GPUs. A full QLoRA training run on an RTX 4090 costs ~$2-5. This is the most cost-effective approach if you fine-tune infrequently.


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