Skip to content
Go back

LLM Fine-Tuning for Mortals: LoRA, QLoRA, and Your Gaming GPU

· Updated:
By SumGuy 12 min read
LLM Fine-Tuning for Mortals: LoRA, QLoRA, and Your Gaming GPU
Contents

So you’ve been playing with ChatGPT, Claude, or some open-source model running on your machine, and you’ve hit a wall. No matter how cleverly you write your system prompt, the model keeps doing that thing you hate. Maybe it won’t stop being corporate. Maybe it doesn’t understand your domain. Maybe you need it to output JSON in a very specific format and it keeps hallucinating extra fields like an overenthusiastic intern.

You’ve heard whispers of “fine-tuning” in Discord servers and Reddit threads. People talk about it the way medieval villagers talked about dragons, with a mixture of fear, respect, and the vague suspicion that you need to be a wizard to attempt it.

Good news: you don’t. Fine-tuning is more accessible than it’s ever been, and you can do it on the same GPU you use to play Elden Ring. Let’s demystify this thing.

First, Let’s Get Our Terms Straight

Prompt engineering is the “just ask nicely” approach: write a good system prompt, give examples, use chain-of-thought, and hope. It’s free and fast, and for a shocking number of use cases it’s enough, as long as the model already knows how to do the thing and just needs steering.

Retrieval-Augmented Generation (RAG) is the “give the model a cheat sheet” approach: stuff relevant documents into the context window at inference time. Great when the model needs facts it wasn’t trained on, your docs, product specs, legal text. It can’t teach a model a new writing style or a consistent output format, though, because the model’s actual behavior never changes, just what it can see.

Fine-tuning is the “change the model’s brain” approach: you keep training a pre-trained model on your own data so it internalizes new patterns, behaviors, or knowledge. Use it when you need consistently different behavior, a specific output format, or domain-specific handling that no amount of prompting reliably delivers. The cost: training data, compute, and the risk of catastrophic forgetting or overfitting if you’re careless.

Prompt engineering gives the model instructions. RAG gives it a textbook. Fine-tuning sends it to school.

The VRAM Problem (And Why Full Fine-Tuning Is Not for You)

A 7-billion parameter model in full precision (float32) takes about 28 GB just to load the weights. Training also needs optimizer states and gradients, so you’re looking at roughly 3-4x the model size in VRAM: 84-112 GB for a 7B model. A 70B model needs a small cluster of A100s that cost more than most people’s cars.

Your RTX 4090 has 24 GB of VRAM. Your RTX 3080 has 10 GB. See the problem?

This is where LoRA enters the chat.

LoRA: The Adapter That Changed Everything

LoRA (Low-Rank Adaptation) asks: what if we didn’t retrain the whole model, and instead added small trainable modules on top while leaving the original weights frozen?

A neural network layer is basically a big matrix multiplication, for a 7B model these can be 4096 x 4096. Full fine-tuning updates every value in that matrix. LoRA’s insight is that the changes needed during fine-tuning are usually low-rank, so they can be approximated by multiplying two much smaller matrices together. Instead of updating a 4096 x 4096 matrix (about 16.7 million parameters), you decompose the update into a 4096 x 16 and a 16 x 4096 matrix (about 131,000 parameters). That’s a 99.2% reduction in trainable parameters.

Think of it like a massive oil painting you want to modify. Full fine-tuning repaints the entire canvas. LoRA places a thin transparent overlay and paints only the changes. The original stays untouched, and you can swap overlays in and out.

This gets you: far less VRAM (you only store gradients for the tiny adapter matrices), faster training (fewer parameters to update), no catastrophic forgetting (the base model stays intact, adapters are modular), and tiny files (a LoRA adapter for a 7B model is 10-50 MB, versus a 14+ GB base model).

Key hyperparameters: r (rank) sets the dimensionality of the low-rank matrices, higher means more expressive but more VRAM (start with 16 or 32). lora_alpha scales the adapter’s influence, lora_alpha = r works fine for most tasks. lora_dropout (typically 0.05-0.1) helps prevent overfitting on small datasets. target_modules picks which layers get adapters, usually the attention projections (q_proj, k_proj, v_proj, o_proj) and sometimes the MLP layers.

QLoRA: When Even LoRA Is Too Chunky

LoRA cuts trainable parameters, but you still load the entire base model for the forward pass. A 7B model in float16 is still about 14 GB, eating most of a 4090’s VRAM before training starts.

QLoRA loads the base model in 4-bit quantization instead. That same 7B model now takes about 3.5-4 GB. The LoRA adapters still train in higher precision (bfloat16), but since they’re tiny that’s not a problem. QLoRA uses NormalFloat4 (NF4) quantization, tuned for the normal-ish distribution of neural network weights, plus double quantization (quantizing the quantization constants themselves) to squeeze out more savings.

ApproachModel VRAMTraining OverheadTotal VRAM
Full fine-tune (fp32)~28 GB~56 GB~84 GB
Full fine-tune (fp16)~14 GB~28 GB~42 GB
LoRA (fp16 base)~14 GB~1-2 GB~16 GB
QLoRA (4-bit base)~4 GB~1-2 GB~6 GB

Six gigabytes. A 3060 with 12 GB can handle that and still leave room for KDE to eat some VRAM in the background. For bigger models: 13B with QLoRA needs ~10-12 GB (fits a 3090 or 4090), 70B needs ~40-48 GB (still needs multiple GPUs or an A100, but a lot better than the ~280 GB full fine-tuning would cost).

Preparing Your Dataset

This is the part people rush, and the part that matters most. Your model will only be as good as the data you train it on.

The standard format is instruction-response pairs. Alpaca format:

{
"instruction": "Summarize the following text in one sentence.",
"input": "The quick brown fox jumped over the lazy dog while the farmer watched from the porch.",
"output": "A fox jumped over a dog while a farmer observed."
}

ChatML / conversational format (also covers the ShareGPT-style {"from": "human"/"gpt"} variant you’ll see in the wild):

{
"conversations": [
{"role": "system", "content": "You are a helpful coding assistant."},
{"role": "user", "content": "Write a Python function to reverse a string."},
{"role": "assistant", "content": "def reverse_string(s):\n return s[::-1]"}
]
}

Most training frameworks handle either with the right configuration. Pick one and be consistent.

How much data? Style/tone transfer: 100-500 good examples. Task-specific behavior: 500-5,000. Domain knowledge injection: 5,000-50,000+. General instruction following: 10,000+. Quality beats quantity every time, 200 carefully crafted examples will outperform 10,000 noisy, repetitive ones.

Common mistakes: examples that are too homogeneous (the model overfits to one pattern), too noisy (typos and wrong answers get learned too), too short (the model struggles to generate longer output), missing system prompts if you want the model to follow them at inference, and forgetting to shuffle an order-biased dataset.

The Hugging Face Ecosystem

Hugging Face is your home base for open-source LLM work. You’ll use transformers (loading and running models), peft (implements LoRA, QLoRA, and other adapters), trl (the SFTTrainer for supervised fine-tuning), datasets (loading and processing data), bitsandbytes (4-bit and 8-bit quantization for QLoRA), and accelerate (distributed training and mixed precision).

Terminal window
pip install torch transformers peft trl datasets bitsandbytes accelerate

Unsloth: The Speed Demon

Unsloth optimizes fine-tuning to run 2-5x faster and use 50-70% less VRAM than standard Hugging Face training. It rewrites the forward and backward passes with custom Triton kernels, cuts unnecessary memory allocations, and fuses operations the standard implementation runs separately. A training run that takes 4 hours on a 4090 might take 90 minutes with Unsloth, using less memory. It’s mostly a drop-in replacement, and it supports Llama, Mistral, Phi, Gemma, and Qwen. If you’re training on one consumer GPU, there’s almost no reason to skip it.

Terminal window
pip install unsloth

Practical Walkthrough: Fine-Tuning with QLoRA and Unsloth

We’ll use Llama 3.1 8B as the base, QLoRA for memory, Unsloth for speed. This runs on an RTX 3090 or 4090.

Step 1: Load the model. load_in_4bit=True is doing a lot of heavy lifting here, it invokes the whole QLoRA quantization pipeline.

from unsloth import FastLanguageModel
import torch
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Meta-Llama-3.1-8B-Instruct",
max_seq_length=2048,
dtype=None, # Auto-detect (will use bf16 if supported)
load_in_4bit=True, # QLoRA: load base model in 4-bit
)

Step 2: Add LoRA adapters. Only about 1-2% of parameters are trainable now. The rest stay frozen in 4-bit, sipping VRAM like a gentleman.

model = FastLanguageModel.get_peft_model(
model,
r=32, # LoRA rank
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_alpha=32,
lora_dropout=0.05,
bias="none",
use_gradient_checkpointing="unsloth", # Saves even more VRAM
)

Step 3: Prepare your dataset, then format it into the chat template your model expects:

from datasets import load_dataset
dataset = load_dataset("your-username/your-dataset", split="train")
# Or: dataset = load_dataset("json", data_files="training_data.jsonl", split="train")
def format_chat(example):
text = tokenizer.apply_chat_template(
example["conversations"], tokenize=False, add_generation_prompt=False,
)
return {"text": text}
dataset = dataset.map(format_chat)

Step 4: Configure training.

from trl import SFTTrainer, SFTConfig
trainer = SFTTrainer(
model=model,
processing_class=tokenizer, # 'tokenizer=' was removed in recent TRL
train_dataset=dataset,
args=SFTConfig(
dataset_text_field="text",
max_length=2048, # 'max_seq_length' was renamed to 'max_length'
per_device_train_batch_size=2,
gradient_accumulation_steps=4, # Effective batch size = 8
warmup_steps=10,
num_train_epochs=3,
learning_rate=2e-4,
fp16=not torch.cuda.is_bf16_supported(),
bf16=torch.cuda.is_bf16_supported(),
logging_steps=10,
output_dir="./outputs",
optim="adamw_8bit", # 8-bit optimizer saves more VRAM
seed=42,
),
)

Heads up: older tutorials pass tokenizer=, dataset_text_field=, and max_seq_length= directly on SFTTrainer with a separate TrainingArguments object. Recent trl (0.16+) moved all of that into SFTConfig and renamed max_seq_length to max_length. If you copy an old snippet and get a TypeError about an unexpected keyword, that’s why.

Step 5: Train. trainer.train(). Go get coffee. On a 4090 with Unsloth, 1,000 examples at 2048 tokens typically finishes in 15-30 minutes, 10,000 examples in 1-3 hours. Your fans will sound like a small aircraft. Normal.

Step 6: Save and test.

model.save_pretrained("./my-fine-tuned-adapter")
tokenizer.save_pretrained("./my-fine-tuned-adapter")
messages = [{"role": "user", "content": "Your test prompt here"}]
inputs = tokenizer.apply_chat_template(
messages, tokenize=True, add_generation_prompt=True, return_tensors="pt",
).to("cuda")
outputs = model.generate(input_ids=inputs, max_new_tokens=256, temperature=0.7)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

Evaluating Your Fine-Tuned Model

Training is half the battle. Run 20-30 representative test prompts and compare outputs before and after, qualitatively, with your own eyes. Watch for two failure modes: overfitting (the model regurgitates training examples verbatim, fix by cutting epochs or adding dropout) and catastrophic forgetting (it’s lost general skills it had before, you trained too aggressively).

On metrics: training loss should decrease and stabilize (near-zero usually means overfitting), perplexity is only meaningful against a validation set, and task-specific metrics (accuracy, F1, BLEU/ROUGE, or plain human judgment) depend on what you’re actually training for.

Merging Adapters

Once you’re happy with a LoRA adapter, you can merge it back into the base model for standalone deployment:

merged_model = model.merge_and_unload()
merged_model.save_pretrained("./my-merged-model")
tokenizer.save_pretrained("./my-merged-model")

With Unsloth, export straight to GGUF for llama.cpp and Ollama:

model.save_pretrained_gguf(
"./my-model-gguf", tokenizer, quantization_method="q4_k_m",
)

Now run it with ollama run or llama-server and never think about Python again. Until next time.

Merge when you have one definitive adapter and want simple deployment. Keep the adapter separate when you swap between multiple LoRAs on the same base model (one for code, one for creative writing, one for support), or when sharing matters, a 50 MB LoRA upload beats a 16 GB merged model every time.

Common Pitfalls and How to Avoid Them

Learning rate too high: training loss spikes or oscillates. Start QLoRA at 2e-4 and work down to 1e-4 or 5e-5 if unstable.

Too many epochs: for small datasets (under 1,000 examples) 1-3 epochs is usually enough. Watch the loss, if it plateaus, stop; if it climbs, you’ve gone too far.

Wrong chat template: Llama uses <|start_header_id|> tokens, Mistral uses [INST], ChatML uses <|im_start|>. Train with the wrong one and inference breaks even with correctly formatted prompts. Always use tokenizer.apply_chat_template().

Sequence length mismatch: examples longer than max_seq_length get truncated silently; much shorter ones waste compute on padding. Check your data’s length distribution first.

Not enough diversity: 500 examples that are all variations of one task make a model great at that task and worse at everything else. Mix in some general instruction-following data.

VRAM OOM mid-training: drop per_device_train_batch_size to 1 and raise gradient_accumulation_steps to compensate, enable gradient checkpointing, shrink max_seq_length, and make sure your browser isn’t hogging GPU memory in the background.

Skipping the before/after baseline: run your evaluation prompts on the base model before fine-tuning too, otherwise “it feels better” is the only metric you’ve got, and that’s not a metric.

Wrapping Up

Fine-tuning used to be something only Big Tech and well-funded startups could do. LoRA and QLoRA changed that: curate quality data, load a base model in 4-bit, attach LoRA adapters to the attention and MLP layers, train with SFTTrainer (Unsloth for speed), evaluate qualitatively and quantitatively, then merge or export.

The barrier to entry dropped from “needs a GPU cluster” to “needs a decent gaming PC,” and the tools keep getting better. Go fine-tune something. Just start with a small model and a small dataset first. Your electricity bill will thank you.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Vaultwarden Organization Sharing: Password Management for Your Whole Household (or Team)
Next Post
Ollama Beyond the Basics: Model Management, Custom Models, and Optimization

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts