Fine-Tuning Small Models: When and Why to Fine-Tune vs. Prompt Engineer
The phrase “fine-tune your model” sounds like the kind of thing that requires a PhD, a cluster of A100s, and six months of runway. In practice, you can meaningfully fine-tune a 7B or 8B parameter model on a consumer GPU in an afternoon, using open-source tooling, and deploy the result on the same laptop you use for everything else.
The harder question isn’t “how do I fine-tune?” — it’s “should I fine-tune, and what for?” Fine-tuning is frequently the wrong tool. It’s also sometimes the only tool that works. Understanding the difference saves a lot of wasted effort.
This guide covers the decision framework first, then the mechanics: LoRA/QLoRA, dataset preparation, training with Unsloth and Axolotl, evaluation, and deployment with Ollama.
Fine-Tuning vs. Prompt Engineering: The Decision
What Prompt Engineering Can Do
Modern large models — Llama 3, Qwen 2.5, Mistral, Gemma 2 — are remarkably capable with nothing but careful prompting. Before considering fine-tuning, exhaust these approaches:
System prompts: A detailed system prompt that defines the persona, constraints, output format, and examples covers the majority of customization needs.
Few-shot examples: Including 3–10 examples of desired input/output pairs in the prompt often achieves near-fine-tuned quality for structured tasks.
Structured output / JSON mode: Most modern models with tool use or JSON mode reliably produce structured output from a schema.
RAG (Retrieval-Augmented Generation): For injecting domain knowledge, retrieving relevant documents at query time is almost always better than baking knowledge into weights.
If the above approaches get you 80% of the way there with acceptable latency and cost, stop. You don’t need fine-tuning.
When Fine-Tuning Actually Wins
Fine-tuning is the right choice in a specific set of scenarios:
1. Style and voice consistency at scale You need every output to sound like it was written by the same author — a specific brand voice, a company’s documentation style, a particular coding style guide. Few-shot examples in context drift over long conversations; a fine-tuned model carries the style into every response by default.
2. Highly structured domain-specific output Your application requires a very specific output format that vanilla models get wrong even with prompting — a custom JSON schema with dozens of fields, a domain-specific markup language, a particular code style with project-specific conventions. Fine-tuning teaches the format once rather than re-teaching it every call.
3. Specialization for a narrow task on a small model You need a 1B or 3B parameter model to perform a task that normally requires a 7B+ model. Fine-tuning a small model on a focused dataset often closes most of the gap, at a fraction of the inference cost.
4. Removing context overhead If you’re spending 2,000 tokens on system prompt and few-shot examples for every call, fine-tuning bakes that in and reduces token usage by 90%. At scale this matters a lot.
5. Teaching genuinely new knowledge (carefully) Models don’t know your internal APIs, your proprietary data formats, your company’s terminology. Fine-tuning can teach this — but RAG is usually better for facts that change, while fine-tuning is better for reasoning patterns and structural knowledge that’s stable.
6. Latency-sensitive edge deployments You need a model running on a Raspberry Pi, a phone, or an embedded device. Fine-tuning a 1B or 3B model to specialize in your task is the path to acceptable quality at those sizes.
When Fine-Tuning Doesn’t Help (and Hurts)
General reasoning and knowledge: Fine-tuning a 7B model on 500 customer support tickets will not teach it to reason better. It might make it more likely to produce responses that look like your customer support tickets. Those are different things.
Hallucination reduction: Fine-tuning often makes hallucinations different, not fewer. If factual accuracy is the goal, RAG or structured tool use is more reliable.
Tasks requiring diverse capabilities: Fine-tuning for a narrow task degrades performance on other tasks. A heavily fine-tuned coding model loses general conversation ability. If you need both, you’re probably better served by a larger base model.
Small datasets (< 50–100 examples): Fine-tuning on very small datasets tends to overfit badly. You’ll get a model that memorized your examples rather than learned from them. With < 100 examples, few-shot prompting almost always wins.
How LoRA and QLoRA Work
Training all the weights of even a 7B model requires more GPU RAM than most people have access to. A 7B model in full precision (fp32) is 28 GB of weights alone, before you account for gradients and optimizer state.
LoRA (Low-Rank Adaptation) solves this by only training a small set of additional weights rather than modifying the original model weights at all.
The insight: weight updates during fine-tuning have low intrinsic rank. Instead of updating a full weight matrix W (shape: d×k, potentially millions of parameters), LoRA decomposes the update as two small matrices: W + ΔW = W + BA, where B is d×r and A is r×k, and r (the rank) is much smaller than d or k. For r=8 and a 4096×4096 weight matrix, you’re training 65,536 parameters instead of 16,777,216 — a 256× reduction.
The original model weights are frozen. You only train the LoRA adapter matrices. At inference time, you can either merge them into the original weights (for deployment) or load them separately (for flexibility, switching between adapters).
QLoRA adds quantization: the frozen base model is loaded in 4-bit precision using bitsandbytes NF4 quantization, while the LoRA adapters train in bf16. A 7B model in 4-bit takes ~5–6 GB of GPU RAM instead of 14+ GB in fp16. This makes fine-tuning practical on a single 8–12 GB consumer GPU (RTX 3080, RTX 4070, etc.) and on Apple Silicon Macs with 16+ GB unified memory.
LoRA Hyperparameters
| Parameter | What It Controls | Typical Values |
|---|---|---|
r (rank) |
Size of adapter matrices; higher = more expressive, more memory | 8, 16, 32, 64 |
lora_alpha |
Scaling factor for adapter; often set to r or 2r | 16, 32, 64 |
lora_dropout |
Regularization on adapter weights | 0.05–0.1 |
target_modules |
Which weight matrices to adapt | q_proj,v_proj or all-linear |
For most practical fine-tuning, r=16, lora_alpha=32, target_modules="all-linear" is a solid starting point. Higher rank gives more capacity at the cost of memory and risk of overfitting.
Preparing Your Dataset
Dataset quality is the single most important factor in fine-tuning success. A good 200-example dataset beats a mediocre 2,000-example one.
Format: Instruction Tuning
The standard format for fine-tuning chat/instruction models is a list of conversations:
|
|
For simpler instruction-response tasks, a flat format works:
|
|
Generating Training Data
Writing hundreds of examples by hand is tedious. Better approaches:
Mine your existing corpus: If you have Slack exports, documentation, code review comments, or customer support tickets, these are your training data. Extract input/output pairs that demonstrate the behavior you want.
Use a large model to generate synthetic data: Prompt GPT-4, Claude, or a large local model to generate training examples from a smaller set of seeds. This works surprisingly well — you’re essentially distilling the larger model’s behavior into your smaller one.
|
|
Quality filtering: After generation, filter aggressively. Remove examples shorter than ~100 characters in the output, examples where the model clearly hallucinated, and near-duplicates. A clean dataset of 300 examples outperforms a noisy dataset of 3,000.
|
|
Dataset Size Guidelines
| Task Complexity | Minimum Examples | Sweet Spot |
|---|---|---|
| Simple format/style | 50–100 | 200–500 |
| Moderate domain adaptation | 200–500 | 500–2,000 |
| Complex reasoning patterns | 500–1,000 | 2,000–5,000 |
| New knowledge injection | 1,000+ | 5,000–20,000 |
For most homelab/personal use cases, 200–1,000 high-quality examples is plenty.
Fine-Tuning with Unsloth
Unsloth is the fastest open-source fine-tuning library, achieving 2–5× speedup over raw Hugging Face training with 60–80% less GPU memory usage. It’s the recommended starting point for most fine-tuning work.
Installation
|
|
For CPU-only or Apple Silicon:
|
|
Training Script
|
|
Run it:
|
|
Training 3 epochs on 500 examples with Llama 3.2 3B on an RTX 3080 (10 GB VRAM) takes about 20 minutes. On an M2 MacBook Pro (16 GB), expect 45–60 minutes via Metal.
Memory Requirements by Model
| Model | Full fp16 | QLoRA 4-bit | Min GPU RAM (QLoRA) |
|---|---|---|---|
| Llama 3.2 1B | 2.5 GB | ~1 GB | 4 GB |
| Llama 3.2 3B | 6.5 GB | ~2.5 GB | 6 GB |
| Llama 3.1 8B | 16 GB | ~5 GB | 8 GB |
| Qwen 2.5 7B | 14 GB | ~4.5 GB | 8 GB |
| Llama 3.1 70B | 140 GB | ~40 GB | 2× A100 |
For homelab fine-tuning, 3B and 7B/8B models are the sweet spot. The 3B models train fast and are usable on very modest hardware; 7B/8B models offer significantly better quality and still fit in consumer GPUs.
Fine-Tuning with Axolotl
Axolotl is more configurable than Unsloth and better for production pipelines. It’s YAML-driven and supports more dataset formats, training strategies, and model architectures out of the box.
Installation
|
|
Configuration File
|
|
Run training:
|
|
Monitor training:
|
|
Evaluating Your Fine-Tuned Model
Before deploying, verify the fine-tuning actually helped and didn’t break things.
Quick Manual Evaluation
|
|
Watching for Overfitting
In your training logs, watch the validation loss alongside training loss:
Epoch 1: train_loss=1.42, eval_loss=1.38
Epoch 2: train_loss=1.21, eval_loss=1.25 ← eval_loss higher than train: early sign
Epoch 3: train_loss=1.05, eval_loss=1.43 ← overfitting — stop here or use epoch 2
If eval loss diverges upward while train loss keeps falling, you’re overfitting. Use the checkpoint from the epoch with the lowest eval loss. This is why save_strategy: "epoch" matters.
For small datasets (< 500 examples), 1–2 epochs often produces better generalization than 3–5.
Task-Specific Metrics
For structured output tasks, write a simple precision/recall evaluator:
|
|
Deploying with Ollama
Once you have a merged model (the base weights + LoRA adapter combined), you can run it locally with Ollama using a Modelfile.
Convert to GGUF
Ollama uses GGUF format. Convert your merged model:
|
|
Quantization levels:
Q8_0— best quality, larger file (~7 GB for 7B)Q5_K_M— excellent quality, smaller (~5 GB)Q4_K_M— good quality, smaller still (~4 GB) — recommended defaultQ3_K_M— acceptable quality, very small (~3 GB)
Create an Ollama Modelfile
|
|
Register with Ollama:
|
|
Test it:
|
|
Serving via API
Once registered, Ollama exposes the model via its standard API:
|
|
Any app already using Ollama can swap in your fine-tuned model by name.
Real-World Use Cases and Realistic Expectations
Use Case 1: Internal Documentation Assistant
Goal: A model that answers questions using your company’s internal APIs, idioms, and terminology — with the right tone and style.
Approach: Fine-tune a 7B model on 500–1,000 examples extracted from your internal docs, Confluence pages, and Slack threads where experts answered questions. The model learns both the vocabulary and the preferred answer style.
What you get: Faster, more accurate answers for common questions. Still hallucinates on edge cases — pair with RAG for factual accuracy.
Use Case 2: Code Style Enforcement
Goal: A coding assistant that generates code matching your team’s exact style (naming conventions, error handling patterns, logging format).
Approach: Extract 200–500 before/after pairs from your code reviews — what was submitted vs. the reviewed version. Fine-tune on these pairs.
What you get: Code completions that fit your codebase. Works best for consistent, rule-based style rather than architectural judgment.
Use Case 3: Structured Data Extraction
Goal: A model that reads unstructured text (log lines, support tickets, emails) and outputs a specific JSON schema.
Approach: Create 300–500 input/output examples with your target schema. A fine-tuned 3B model often matches or exceeds a prompted 7B model for this task.
What you get: Fast, cheap, accurate extraction. Very reliable JSON output. Fragile to inputs that are very different from training examples.
What to Expect Honestly
Fine-tuning a small model on a specialized task produces a model that:
- Is noticeably better than the base model on in-distribution examples
- Behaves more consistently and predictably
- Is faster and cheaper to run than larger prompted models
- Still fails on out-of-distribution inputs
- Can degrade on tasks far from its training distribution
It does not produce a model that:
- Is smarter or more capable in any fundamental sense
- Reliably handles situations it hasn’t seen
- Is immune to hallucination
- Approaches GPT-4/Claude-class quality on complex reasoning
Fine-tuning specializes; it doesn’t upgrade. Keep that expectation clear and you’ll find the right problems to apply it to.
The Full Workflow in Summary
1. Define the task precisely
└── "I need a model that converts SQL queries to our
internal query builder syntax"
2. Exhaust prompting first
└── Can a system prompt + few-shot examples do it well enough?
└── If yes: stop here
3. Build dataset
├── Mine existing examples from your codebase/docs
├── Generate synthetic data with a large model
└── Filter aggressively for quality
4. Fine-tune with Unsloth
├── Start with 3B model + QLoRA + r=16
├── Train 2–3 epochs, watch eval loss
└── Save best checkpoint (lowest eval loss)
5. Evaluate
├── Manual comparison with base model on test prompts
└── Automated accuracy on held-out test set
6. Deploy with Ollama
├── Convert to GGUF (Q4_K_M for balance)
├── Register with Modelfile
└── Serve via Ollama API
7. Iterate
└── More data, better data, or different hyperparameters
if quality isn't there
The whole cycle from dataset to deployed model can be done in a weekend. The bottleneck is almost always dataset quality, not the training process.
Comments