Fine-Tuning LLMs on Consumer Hardware: LoRA Guide 2024
Train custom models on your GPU using LoRA/QLoRA — no cloud credits needed.
Introduction
Fine-tuning used to require A100s and thousands of dollars. With LoRA (Low-Rank Adaptation) and QLoRA (Quantized LoRA), you can fine-tune 7B-70B models on a single 24GB GPU (RTX 3090/4090) or even 12GB with quantization.
What is LoRA?
LoRA freezes the base model and trains only small adapter matrices:
Base Model (frozen) + LoRA Adapters (trainable) = Fine-tuned Model
Benefits:
- 100-1000x fewer trainable parameters
- Fast training (minutes to hours)
- Small checkpoint files (MBs not GBs)
- Mergeable back into base model
Hardware Requirements
| Model Size | Method | VRAM Needed | GPU Example |
|---|---|---|---|
| 7B | LoRA (fp16) | ~16 GB | RTX 3090/4090 |
| 7B | QLoRA (4-bit) | ~8 GB | RTX 3060 12GB |
| 13B | QLoRA (4-bit) | ~12 GB | RTX 3080 12GB |
| 33B | QLoRA (4-bit) | ~20 GB | RTX 3090/4090 |
| 70B | QLoRA (4-bit) | ~40 GB | Dual 3090/4090 |
Setup Environment
# Create environment
conda create -n lora python=3.10 -y
conda activate lora
# Install dependencies
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install transformers peft trl accelerate bitsandbytes datasets wandb
Prepare Dataset
// data/train.jsonl
{"text": "### Instruction:\nWrite a Python function to reverse a string\n### Response:\ndef reverse_string(s):\n return s[::-1]"}
{"text": "### Instruction:\nExplain what a decorator does in Python\n### Response:\nA decorator is a function that takes another function and extends its behavior..."}
Format: Alpaca/ShareGPT style with ### Instruction: and ### Response: markers.
Training Script (QLoRA)
# train_lora.py
import os
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
# Config
MODEL_ID = "mistralai/Mistral-7B-v0.1" # or "meta-llama/Llama-2-7b-hf"
OUTPUT_DIR = "./mistral-7b-code-lora"
DATA_PATH = "data/train.jsonl"
# 4-bit quantization config
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True
)
# Load model
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True
)
model = prepare_model_for_kbit_training(model)
# LoRA config
lora_config = LoraConfig(
r=64, # Rank (higher = more capacity)
lora_alpha=16, # Scaling factor
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 = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Trainable params: ~0.5% of total
# Tokenizer
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# Dataset
dataset = load_dataset("json", data_files=DATA_PATH, split="train")
# Training arguments
training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
warmup_steps=10,
max_steps=100, # Adjust for your dataset
learning_rate=2e-4,
fp16=True,
logging_steps=5,
save_strategy="steps",
save_steps=50,
report_to="wandb", # or "none"
optim="paged_adamw_8bit"
)
# Trainer
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=512,
tokenizer=tokenizer,
args=training_args,
packing=False
)
# Train
trainer.train()
# Save
trainer.model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
print(f"Model saved to {OUTPUT_DIR}")
Run Training
# Single GPU
python train_lora.py
# Multi-GPU (if available)
accelerate launch --multi_gpu train_lora.py
Merge & Export
# merge_lora.py
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
BASE_MODEL = "mistralai/Mistral-7B-v0.1"
LORA_PATH = "./mistral-7b-code-lora"
OUTPUT_PATH = "./mistral-7b-code-merged"
# Load base
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
torch_dtype=torch.float16,
device_map="auto"
)
# Load & merge LoRA
model = PeftModel.from_pretrained(model, LORA_PATH)
model = model.merge_and_unload()
# Save merged
model.save_pretrained(OUTPUT_PATH)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
tokenizer.save_pretrained(OUTPUT_PATH)
Convert to GGUF (llama.cpp)
# Install llama.cpp
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp && make
# Convert
python convert-hf-to-gguf.py ../mistral-7b-code-merged \
--outfile ../mistral-7b-code.Q4_K_M.gguf \
--outtype q4_k_m
Inference with Ollama
# Create Modelfile
cat > Modelfile << 'EOF'
FROM ./mistral-7b-code.Q4_K_M.gguf
TEMPLATE "{{ .System }}\n\n{{ .Prompt }}"
PARAMETER temperature 0.7
PARAMETER top_p 0.9
EOF
# Create model
ollama create mistral-code -f Modelfile
# Test
ollama run mistral-code "Write a Python decorator for timing functions"
Cost Breakdown (RTX 3090 24GB)
| Item | Cost |
|---|---|
| Electricity (4 hrs @ 350W) | ~$0.20 |
| GPU depreciation | ~$1.00 |
| Total per run | ~$1.20 |
vs. Cloud (A100 40GB): $1.50-3.00/hr → $6-12 for same training.
Tips for Success
- Dataset quality > quantity — 500 clean examples beat 5000 noisy ones
- Start small — 100 steps, check loss, then scale
- Monitor VRAM — Reduce batch size if OOM
-
Use gradient checkpointing —
model.gradient_checkpointing_enable() - Eval regularly — Keep a validation set
Common Issues
| Error | Fix |
|---|---|
| CUDA OOM | Reduce batch size, increase grad accumulation, use 4-bit |
| Loss not decreasing | Increase learning rate, check data format |
| NaN loss | Lower learning rate, add gradient clipping |
| Slow training | Enable flash attention, use packing=True
|
Conclusion
LoRA/QLoRA democratized LLM fine-tuning. With a consumer GPU, you can create domain-specialized models for coding, writing, analysis — whatever your use case. Start with 7B QLoRA, iterate on data, and deploy locally via Ollama or llama.cpp.
Next: Try DPO/ORPO for preference alignment, or merge multiple LoRAs for multi-task models.
Part of the LLM Fine-Tuning series on Dev.to
Top comments (0)