Fine-Tuning LLMs: When to Do It and How to Do It Right
โก Quick Answer
Fine-tuning LLMs explained โ when fine-tuning beats prompting, how to prepare data, run LoRA fine-tuning with minimal GPU, and evaluate results with real cost and time estimates.
Get more content like this on Telegram!
Daily AI tips, notes & resources โ free
Advertisement
Fine-Tuning LLMs: When to Do It and How to Do It Right
Fine-tuning means retraining part of a language model's weights on your own examples so it learns a specific format, style, or behavior. Think of a base model as a generalist doctor and fine-tuning as a residency in one specialty โ same underlying knowledge, sharper on one task.
Most of the time you don't need it. Good prompting on a frontier model like GPT-4o or Claude 3.5 Sonnet is faster to build, easier to maintain, and often performs just as well.
But for a narrow set of problems โ rigid output formats, high-volume cost control, domain-specific tone โ fine-tuning wins outright. This guide covers when it's worth it, how to run it cheaply with LoRA, and how to prove it actually helped.
When to Fine-Tune vs. Prompt
Prompting beats fine-tuning for roughly 99% of tasks. It costs nothing to iterate, ships in minutes, and a well-built few-shot prompt closes most of the quality gap. Exhaust it before touching a training loop.
# Few-shot prompting often achieves surprisingly strong results
system_prompt = """
You are a support ticket classifier. Classify tickets into:
billing, technical, account, feature_request, other.
Examples:
Input: "My invoice shows wrong amount"
Output: billing
Input: "App crashes when I upload PDF files"
Output: technical
Input: "How do I change my password?"
Output: account
Respond with only the category label, nothing else.
"""
# This approach may already achieve 90%+ accuracy
# without any fine-tuningFine-tune when
- Output format must be exact. Extracting structured JSON from medical notes fails when the model adds commentary or renames a field, and that breaks downstream code. 500 note-to-JSON pairs fixes it โ 100% format compliance instead of "usually correct."
- Domain style is non-negotiable. A legal drafting firm needs jurisdiction-specific formatting and terminology no generic prompt reliably enforces. Fine-tuning on 2,000 firm documents bakes the house style in permanently.
- Volume makes prompts expensive. At 1M requests a day, a 1,500-token system prompt burns 1.5B tokens daily โ about $7,500 at $5/M. A fine-tuned Llama 3.1 8B learns the task once and runs for roughly $50/day in compute.
- A small model must match a big one. You can't run a 70B model on a phone. Fine-tune a 7B model on 1,000 examples of the 70B model's outputs โ a technique called knowledge distillation โ and the small model performs at the big one's level for that one task.
Scenario: Extracting structured JSON from medical notes
Problem: GPT-4 sometimes adds explanatory text, changes field names,
or omits optional fields โ inconsistency breaks downstream code
Solution: Fine-tune on 500 medical note โ JSON pairs
Result: 100% output format complianceScenario: Legal document drafting firm
Problem: Model uses consumer-friendly language instead of legal terminology;
doesn't follow jurisdiction-specific formatting conventions
Solution: Fine-tune on 2,000 firm documents with their preferred style
Result: Outputs match firm style guide without extensive promptingScenario: High-volume classification (1M requests/day)
Problem: Each request needs a 1,500-token system prompt with examples
Cost: 1.5B tokens/day at $5/M = $7,500/day
Solution: Fine-tune Llama 3.1 8B to learn the task
Deploy locally or on cheaper inference endpoint
Cost: $50/day in compute vs $7,500/day in API costsScenario: Mobile/edge deployment
Problem: Can't run a 70B model on device
Solution: Fine-tune a 7B model on 1,000 examples of the 70B model's outputs
(knowledge distillation approach)
Result: 7B model performs at 70B level for the specific taskDon't fine-tune when
- Prompting already works. If few-shot examples hit your accuracy bar, training a model adds maintenance cost for no gain.
- You have fewer than 100 examples. Too little data to teach a stable pattern โ the model overfits to noise.
- You need new facts, not new behavior. Fine-tuning bakes in style and format, not knowledge. Use RAG to ground answers in current, retrievable facts.
- You need to iterate fast. A prompt edit is instant; a fine-tuning run takes hours to days. Don't pay that cost during early experimentation.
The Fine-Tuning Stack in 2026
Three tools cover almost every fine-tuning project today: Unsloth, Hugging Face TRL, and the OpenAI fine-tuning API. Pick based on hardware access and how much control you need.
Popular combinations:
- Unsloth + Llama 3.1 8B + QLoRA: fastest, most memory-efficient
- Hugging Face TRL + any model: most flexible, best ecosystem
- OpenAI fine-tuning API: simplest if you use GPT-3.5/GPT-4o mini
Hardware requirements (QLoRA):
- 7B model fine-tuning: 12-16GB VRAM (RTX 3090, A10G, T4)
- 13B model fine-tuning: 24GB VRAM (A100 40GB, RTX 4090)
- 70B model fine-tuning: 48-80GB VRAM or multi-GPUData Preparation
Data quality determines fine-tuning outcomes more than any hyperparameter. A model trained on 500 clean, consistent examples will beat one trained on 5,000 messy ones โ garbage in, garbage memorized.
# Training data format (Alpaca/instruction-following style)
import json
training_examples = [
{
"instruction": "Classify this support ticket",
"input": "My payment was charged twice for the same order",
"output": "billing"
},
{
"instruction": "Classify this support ticket",
"input": "I can't log in after resetting my password",
"output": "account"
}
]
# Check data quality
def validate_training_data(examples):
issues = []
for i, ex in enumerate(examples):
if not ex.get('instruction'):
issues.append(f"Example {i}: missing instruction")
if not ex.get('output'):
issues.append(f"Example {i}: missing output")
if len(ex.get('output', '')) == 0:
issues.append(f"Example {i}: empty output")
return issues
issues = validate_training_data(training_examples)
print(f"Issues found: {len(issues)}")
if issues:
for issue in issues:
print(f" - {issue}")
# Save in JSONL format
with open('training_data.jsonl', 'w') as f:
for example in training_examples:
f.write(json.dumps(example) + '\n')
print(f"Training examples: {len(training_examples)}")Data Quality Checklist
โก Consistent output format (exact same structure in every example)
โก No contradictions (two examples with same input but different output)
โก Representative distribution (covers all cases you'll see in production)
โก Edge cases included (examples with unusual inputs)
โก Held-out test set (10-20% never used in training)
โก Balanced classes (for classification tasks)
โก Quality > quantity (500 excellent >> 5,000 mediocre)Fine-Tuning with Unsloth + QLoRA
Unsloth is a training library that patches the fine-tuning process to run faster and use less GPU memory, with no accuracy tradeoff. Paired with QLoRA โ LoRA on a 4-bit quantized base model โ it fits an 8B model's fine-tuning run on a single consumer GPU.
# Install
# pip install unsloth
from unsloth import FastLanguageModel
import torch
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset
# 1. Load base model with QLoRA configuration
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/Meta-Llama-3.1-8B-Instruct",
max_seq_length = 2048,
dtype = None, # Auto-detect: float16 or bfloat16
load_in_4bit = True, # QLoRA: quantize to 4-bit for memory efficiency
)
# 2. Add LoRA adapters
model = FastLanguageModel.get_peft_model(
model,
r = 16, # LoRA rank (higher = more parameters = better but slower)
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha = 16,
lora_dropout = 0, # 0 is optimal for LoRA
bias = "none",
use_gradient_checkpointing = "unsloth",
random_state = 42,
)
print(f"Trainable parameters: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}")
# 3. Load and format dataset
dataset = load_dataset("json", data_files="training_data.jsonl", split="train")
# Format into chat template
def format_prompt(example):
return f"""### Instruction:
{example['instruction']}
### Input:
{example['input']}
### Response:
{example['output']}"""
# 4. Training configuration
trainer = SFTTrainer(
model = model,
tokenizer = tokenizer,
train_dataset = dataset,
dataset_text_field = "text",
max_seq_length = 2048,
args = TrainingArguments(
output_dir = "./outputs",
num_train_epochs = 3,
per_device_train_batch_size = 4,
gradient_accumulation_steps = 4, # Effective batch size = 16
warmup_steps = 10,
learning_rate = 2e-4,
fp16 = not torch.cuda.is_bf16_supported(),
bf16 = torch.cuda.is_bf16_supported(),
logging_steps = 10,
optim = "adamw_8bit",
weight_decay = 0.01,
lr_scheduler_type = "cosine",
seed = 42,
),
)
# 5. Train
trainer_stats = trainer.train()
print(f"Training time: {trainer_stats.metrics['train_runtime']:.1f}s")
print(f"Training loss: {trainer_stats.metrics['train_loss']:.4f}")
# 6. Save adapter
model.save_pretrained("my_finetuned_model")
tokenizer.save_pretrained("my_finetuned_model")Inference with Your Fine-Tuned Model
from unsloth import FastLanguageModel
# Load the fine-tuned model
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "my_finetuned_model",
max_seq_length = 2048,
dtype = None,
load_in_4bit = True,
)
# Enable fast inference
FastLanguageModel.for_inference(model)
# Generate response
def classify_ticket(ticket_text):
prompt = f"""### Instruction:
Classify this support ticket
### Input:
{ticket_text}
### Response:
"""
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=20,
temperature=0.1, # Low temperature for consistent classification
do_sample=True,
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# Extract only the response part
category = response.split("### Response:")[-1].strip()
return category
# Test
print(classify_ticket("My account shows duplicate charges from last week"))
# Output: billingOpenAI Fine-Tuning API (Simpler Option)
The OpenAI fine-tuning API trains a custom version of a GPT model without managing any GPU infrastructure yourself. Upload a JSONL file, start a job, and OpenAI handles the rest โ the tradeoff is less control over hyperparameters than a self-hosted run.
from openai import OpenAI
import json
client = OpenAI()
# 1. Prepare data in OpenAI format
training_data = [
{
"messages": [
{"role": "system", "content": "You are a support ticket classifier."},
{"role": "user", "content": "My payment failed twice"},
{"role": "assistant", "content": "billing"}
]
},
# ... more examples
]
# Save as JSONL
with open('openai_training.jsonl', 'w') as f:
for example in training_data:
f.write(json.dumps(example) + '\n')
# 2. Upload training file
with open("openai_training.jsonl", "rb") as f:
response = client.files.create(file=f, purpose="fine-tune")
file_id = response.id
print(f"File uploaded: {file_id}")
# 3. Create fine-tuning job
job = client.fine_tuning.jobs.create(
training_file=file_id,
model="gpt-4o-mini-2024-07-18", # Cheapest, fastest to fine-tune
hyperparameters={"n_epochs": 3}
)
print(f"Fine-tuning job: {job.id}")
# 4. Check status
import time
while True:
status = client.fine_tuning.jobs.retrieve(job.id)
print(f"Status: {status.status}")
if status.status in ["succeeded", "failed"]:
break
time.sleep(30)
if status.status == "succeeded":
fine_tuned_model_id = status.fine_tuned_model
print(f"Fine-tuned model: {fine_tuned_model_id}")Evaluation
Evaluation means comparing your fine-tuned model against the base model on data neither has seen. Skip this step and you're deploying on faith โ a fine-tuned model can look better in a few spot checks and still underperform on the full distribution.
import json
from openai import OpenAI
def evaluate_model(model_id, test_data, client):
"""Compare fine-tuned model vs base model on test set"""
results = {
'fine_tuned': {'correct': 0, 'total': 0},
'base': {'correct': 0, 'total': 0}
}
for example in test_data:
question = example['input']
ground_truth = example['output']
for model_name in ['fine_tuned', 'base']:
model = model_id if model_name == 'fine_tuned' else 'gpt-4o-mini'
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "Classify support tickets."},
{"role": "user", "content": question}
],
max_tokens=10,
temperature=0
)
prediction = response.choices[0].message.content.strip()
results[model_name]['total'] += 1
if prediction.lower() == ground_truth.lower():
results[model_name]['correct'] += 1
for model_name, r in results.items():
accuracy = r['correct'] / r['total']
print(f"{model_name}: {accuracy:.1%} ({r['correct']}/{r['total']})")
evaluate_model(fine_tuned_model_id, test_data, client)Cost Estimates
Fine-tuning costs fall into two buckets: managed API fees or self-hosted GPU time. Both are cheap relative to what most teams expect โ the real cost is engineering time spent preparing data.
OpenAI fine-tuning (gpt-4o-mini):
- Training: $0.003/1K tokens
- 1,000 examples ร 300 tokens avg ร 3 epochs = 900K tokens = $2.70
- Inference: $0.30/1M input + $1.20/1M output (same price as regular)
Self-hosted QLoRA (Llama 3.1 8B):
- Cloud GPU for training: ~$1-3/hour (A10G or A100)
- 1,000 examples ร 3 epochs: ~1-2 hours = $2-6
- Inference: fixed server cost (much cheaper at scale)
Time requirements:
- 100-500 examples: 30-60 min fine-tuning
- 1,000-5,000 examples: 1-4 hours
- 10,000+ examples: 4-12+ hoursThe Bottom Line
Fine-tuning is powerful but rarely the first move. Most tasks that seem to need it are solved by a well-structured few-shot prompt โ try that first.
When fine-tuning is justified, QLoRA with Unsloth cuts the hardware bar to a single consumer GPU. A fine-tuned 7B model can outperform a prompted 70B model on a specific task, at a fraction of the inference cost.
For the broader LLM context, see our how LLMs work guide. For using LLMs in applications without fine-tuning, see our RAG guide.
Further Reading
- Multimodal AI Explained: How Models Process Text, Images, Audio, and Video
- Ollama Tutorial: Run LLMs Locally on Your Computer (Complete Setup Guide)
- RLHF Explained: How Human Feedback Trains AI to Be Helpful and Safe
- Transformer Architecture Explained: The Architecture Behind All Modern AI
- GPT-4 vs Claude vs Gemini: Which AI Model Is Best in 2025?
- Math for Machine Learning: What You Actually Need (and What You Don't)
- ChatGPT for Excel: Automate Spreadsheets in Seconds
- The Complete Prompt Engineering Guide for 2025 (With 100 Examples)
Advertisement
๐ฌ DiscussionPowered by GitHub Discussions
Frequently Asked Questions

Software Testing Expert & Prompt Engineering
Ensures every release is bug-free through rigorous testing, and crafts high-precision prompts that power our AI-driven workflows. Abdullah Al Arman Emon leads QA and prompt engineering across AiTechWorlds.
Not sure yet? Ask AI about this article
Get an instant, unbiased AI summary of โFine-Tuning LLMs: When to Do It and How to Do It Rightโ.
Advertisement
Related Articles
AI Hallucination Explained: Why LLMs Make Things Up (and How to Fix It)
AI hallucination explained โ why large language models confidently generate false facts, how to detect it, and practical mitigation strategies for production systems.
Embeddings Explained: How AI Converts Words to Numbers That Mean Something
Embeddings explained โ how LLMs convert text, images, and code into vector representations that capture meaning, enable semantic search, and power recommendation systems.
GPT-4 vs Claude vs Gemini: Which AI Model Is Best in 2026?
GPT-4 vs Claude vs Gemini comparison for 2026 โ honest benchmarks, real-world performance across coding, writing, analysis, and reasoning, and which model to use for each task.
How Large Language Models Work: A Clear Technical Explanation
How large language models work explained clearly โ from tokenization and transformers to training on billions of tokens, RLHF alignment, and why they sometimes hallucinate.