TutorialModels & inference
Fine tune LLM LoRA adapters: data, training and a first test on one GPU
Fine tune LLM LoRA adapters on one GPU: check that fine-tuning fits, prepare data, train with PEFT and TRL or Unsloth, and compare against the base model.
To fine tune LLM LoRA adapters on a single GPU, write examples of the exact task you want, train a small adapter with Hugging Face PEFT and the TRL SFTTrainer, and compare its answers with the base model on examples it never saw. Before any of that, confirm that fine-tuning is the right fix, because many problems that look like model limits are prompt or retrieval problems.
This guide uses TRL 1.15.0 and PEFT 0.21.0, the versions the Hugging Face docs showed when it was written. The model in the examples is Qwen/Qwen3-0.6B, the one TRL uses in its own quick start.
Fine-tuning, RAG or a better prompt: which problem do you have?
Fine-tuning changes how a model behaves. It is a poor way to give a model facts, because facts change and a trained model cannot cite where it learned them. Use this decision table before you collect any data. It is a rule of thumb, not a benchmark result.
| Symptom | Try first | Why |
|---|---|---|
| The model does not know your product, docs or recent events | Retrieval (RAG) | Facts change; retrieval updates without retraining and can show sources |
| Answers need citations | Retrieval (RAG) | A fine-tuned model has no record of where a fact came from |
| The model ignores a format you described once | A better prompt with examples or structured output | Cheap to try and easy to undo |
| Your prompt has grown to dozens of examples and still drifts | Fine-tuning | The examples move into the weights instead of every request |
| You need a small, cheap model to do one narrow task a large model already does well | Fine-tuning a small model | Outputs from the large model can become the training set |
| You can show the behavior in examples but cannot describe it in rules | Fine-tuning | Supervised training learns from pairs, not instructions |
| You have fewer than a handful of good examples | Prompting | There is nothing yet to train on or to test against |
If two rows apply, combine them. A fine-tuned model can still sit behind a retrieval step.
What LoRA changes in a model
LoRA, from the 2021 paper LoRA: Low-Rank Adaptation of Large Language Models (opens in a new tab), freezes the pretrained weights and trains a pair of small matrices next to selected layers. The PEFT LoRA reference (opens in a new tab) describes the pair as update matrices that represent the weight change through low-rank decomposition. At inference, the original and adapted weights are combined.
Three details matter in practice:
- Rank. The
rsetting sets the size of the update matrices. A higher rank means more trainable parameters and more capacity. The PEFT default isr=8. - Scaling. The update is scaled by
lora_alpha/r, or bylora_alpha/math.sqrt(r)withuse_rslora=True. The defaultlora_alphais 8. - Starting point. By default PEFT sets the B matrix to zero, so an untrained adapter changes nothing.
The trainable share is tiny. The PEFT quicktour (opens in a new tab) wraps the q_proj layers of meta-llama/Llama-3.2-1B at rank 8 and reports 524,288 trainable parameters out of 1,236,338,688. The saved adapter holds only the trained weights: the quicktour's opt-350m example has an adapter_model.safetensors file of 6.3MB.
Preparing a training set
The TRL SFT Trainer docs (opens in a new tab) list two data types the trainer accepts: language modeling data (a text field or a messages list) and prompt-completion data. For a single task, conversational prompt-completion is the cleanest choice. By default the trainer computes loss only on the completion, so the model learns to answer and does not spend capacity learning to repeat prompts.
Write one JSON object per line. The ticket-triage task below is illustrative.
{"prompt": [{"role": "user", "content": "Triage: The CSV export returns a 500 error since this morning."}], "completion": [{"role": "assistant", "content": "{\"category\": \"bug\", \"priority\": \"high\"}"}]}
{"prompt": [{"role": "user", "content": "Triage: Could you add dark mode to the dashboard?"}], "completion": [{"role": "assistant", "content": "{\"category\": \"feature_request\", \"priority\": \"low\"}"}]}
When the data is conversational, the trainer applies the model's chat template for you. Split the data into three files before training: train.jsonl, valid.jsonl for the loss curve, and test.jsonl that training never touches. A few checks pay off before the first run:
- Every completion is an answer you would accept in production.
- The label set is balanced enough that the model cannot score well by always guessing one class.
- No test prompt appears, even reworded, in the training file.
- Examples fit within
max_length. TRL defaults to 1024 tokens and truncates longer sequences.
How to fine tune LLM LoRA adapters with PEFT and TRL
Install the three libraries. Each install command comes from that library's own docs.
pip install trl
pip install peft
pip install datasets
Then pass a LoraConfig to the trainer through peft_config. TRL wraps the model, trains only the adapter and saves it.
# train.py
import torch
from datasets import load_dataset
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
data = load_dataset(
"json",
data_files={"train": "train.jsonl", "validation": "valid.jsonl"},
)
peft_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules="all-linear",
task_type="CAUSAL_LM",
)
args = SFTConfig(
output_dir="qwen3-tickets-lora",
learning_rate=1e-4,
num_train_epochs=3,
eval_strategy="epoch",
model_init_kwargs={"dtype": torch.bfloat16},
)
trainer = SFTTrainer(
model="Qwen/Qwen3-0.6B",
args=args,
train_dataset=data["train"],
eval_dataset=data["validation"],
peft_config=peft_config,
)
trainer.train()
trainer.save_model()
The rank and alpha values are a starting point to tune, not a recommendation from the docs. A few other settings are worth knowing:
target_modules="all-linear"applies LoRA to every linear layer except the output layer. The PEFT guide notes this matches the QLoRA approach.learning_rate=1e-4follows TRL's tip for adapters. TheSFTConfigdefault is2e-5, which suits full fine-tuning.model_init_kwargssets the load precision. Without adtype, TRL loads the model infloat32.bf16defaults toTruewhenfp16is not set. On a GPU without bfloat16 support, setfp16=Trueand load intorch.float16instead.gradient_checkpointingdefaults toTrueinSFTConfig, which saves memory at some cost in speed.
For QLoRA, SFTTrainer also takes a quantization_config, which TRL documents as the way to combine quantized loading with peft_config.
Unsloth as the faster option
Unsloth (opens in a new tab) is an open-source library for training and running models that works with TRL's SFTTrainer. Its README claims fine-tuning that is 2x faster with 70% less VRAM and no accuracy loss. TRL's integration page quotes up to 80% less VRAM. Both figures are the project's own.
The change to the script is small. You load the model through FastLanguageModel and add LoRA with its get_peft_model, then hand the result to SFTTrainer without a peft_config. This version follows TRL's Unsloth integration page, which loads a 7B Mistral model in 4-bit.
from trl import SFTConfig, SFTTrainer
from unsloth import FastLanguageModel
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/mistral-7b",
max_seq_length=2048,
dtype="auto",
load_in_4bit=True,
)
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing=True,
random_state=3407,
)
trainer = SFTTrainer(
model=model,
args=SFTConfig(output_dir="./output", max_length=2048),
train_dataset=dataset,
)
trainer.train()
TRL's page installs it with pip install unsloth. The README's current Linux route uses a uv environment and uv pip install unsloth --torch-backend=auto. Unsloth also ships a web UI, Unsloth Studio, started with unsloth studio. The core package is Apache 2.0, and optional parts such as Studio are AGPL-3.0.
Testing the adapter against the base model
Training loss only says the model fits the training data. The test that matters is whether the adapter beats the base model on test.jsonl. PEFT makes this cheap: disable_adapter() is a context manager that runs the same loaded model as the base model, so you only load one copy.
# compare.py
import json
from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
model = AutoPeftModelForCausalLM.from_pretrained("qwen3-tickets-lora")
model.eval()
def reply(messages):
inputs = tokenizer.apply_chat_template(
messages, tokenize=True, add_generation_prompt=True, return_tensors="pt"
).to(model.device)
outputs = model.generate(**inputs, max_new_tokens=128, do_sample=False)
new_tokens = outputs[0][inputs["input_ids"].shape[1]:]
return tokenizer.decode(new_tokens, skip_special_tokens=True)
with open("test.jsonl") as f:
for line in f:
row = json.loads(line)
tuned = reply(row["prompt"])
with model.disable_adapter():
base = reply(row["prompt"])
expected = row["completion"][0]["content"]
print(json.dumps({"expected": expected, "base": base, "tuned": tuned}))
Score the output with a rule that fits the task. For the triage example, parse the JSON and count exact matches on category and priority. Also run a handful of general prompts outside the task. A narrow adapter that breaks ordinary answers can still be the right result, but you should know before you ship it.
Merging, exporting and running it locally
An adapter needs its base model at load time. To get one standalone model, merge the adapter into the base weights. The PEFT docs stress that merge_and_unload() returns a new model rather than working in place, so assign the result.
# merge.py
from peft import AutoPeftModelForCausalLM
from transformers import AutoTokenizer
model = AutoPeftModelForCausalLM.from_pretrained("qwen3-tickets-lora")
merged = model.merge_and_unload()
merged.save_pretrained("qwen3-tickets-merged")
AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B").save_pretrained("qwen3-tickets-merged")
To run the merged model in Ollama, point a Modelfile at the directory. Ollama's Modelfile docs say the directory must hold Safetensors weights for a supported architecture, so check that list for your base model.
FROM ./qwen3-tickets-merged
ollama create qwen3-tickets -f ./Modelfile
ollama run qwen3-tickets
If you trained with Unsloth, it can write GGUF files directly through llama.cpp, for example model.save_pretrained_gguf("directory", tokenizer, quantization_method="q4_k_m"). A Modelfile can then use FROM with the path to the .gguf file it writes. For other runners such as LM Studio, check which formats they import before you choose an export path.
Frequently asked questions
How many training examples do I need for a LoRA fine-tune?
None of the PEFT, TRL or Unsloth pages used for this guide gives a minimum, so treat any fixed number with suspicion. Start with what you have, keep a held-out test file, and train again with more data if the adapter does not beat the base model on it. The comparison script tells you whether more data is helping.
Can I keep several adapters for one base model?
Yes. PEFT can load more than one adapter onto a base model with `load_adapter()` and switch between them with `set_adapter()`. Unmerged adapters are small files, so one base model can serve several narrow tasks. Merging is only needed when you want a single standalone model.
What are DoRA and rsLoRA, and should I turn them on?
Both are flags on `LoraConfig`. `use_rslora=True` changes the scaling factor to `lora_alpha/math.sqrt(r)`, and `use_dora=True` splits the weight update into magnitude and direction. The PEFT docs say DoRA can help especially at low ranks but adds overhead, so they recommend merging weights for inference. Leave both off for a first run and compare them later on the same test set.
Does Unsloth work on Macs and AMD GPUs?
The Unsloth README says it works on Windows, Linux, WSL and macOS and lists NVIDIA, AMD and Intel GPUs, CPUs and a Vulkan backend. TRL's integration page has a line that names NVIDIA GPUs and says AMD and Intel support is coming soon. Check the README for the current install route on your hardware.
Sources
- 1.PEFT · Hugging FaceOfficial docsPrimaryhuggingface.co
- 2.LoRA · Hugging FaceOfficial docsPrimaryhuggingface.co
- 3.SFT Trainer · Hugging FaceOfficial docsPrimaryhuggingface.co
- 4.GitHub - unslothai/unsloth: Local UI to run and train LLMs and diffusion models. Supports GGUF, MLX, Qwen3.8, DeepSeek-V4, MiniMax-H3, Gemma 4, FLUX and more. · GitHubRepositoryPrimarygithub.com
- 5.huggingface.co/docs/trl/unsloth_integrationRepositoryPrimary
- 6.huggingface.co/docs/peft/developer_guides/loraRepositoryPrimary
- 7.huggingface.co/docs/peft/package_reference/peft_modelRepositoryPrimary
- 8.huggingface.co/docs/peft/quicktourRepositoryPrimary
- 9.huggingface.co/docs/peft/installRepositoryPrimary
- 10.huggingface.co/docs/trl/installationRepositoryPrimary
- 11.huggingface.co/docs/datasets/installationRepositoryPrimary
- 12.huggingface.co/docs/datasets/loadingRepositoryPrimary
- 13.huggingface.co/docs/transformers/chat_templatingRepositoryPrimary
- 14.arxiv.org/abs/2106.09685PaperPrimary
- 15.github.com/ollama/ollama/blob/main/docs/modelfile.mdxRepositoryPrimary
Spotted an error? See our corrections policy.
Enjoyed this? Get the weekly brief.
New guides, Project & Tool of the Week and the AI developments that matter. One email, no spam.
Read next
- GuideModels & inference
Prompt caching with the OpenAI, Claude and Gemini APIs: what is cached and what breaks it
Compare prompt caching API rules for OpenAI, Claude and Gemini, order prompts so the cache hits, avoid silent misses and measure savings from usage fields.
8 min read
NewsModels & inferenceIntel details the GPU D270, an air-cooled inference card with up to 480 GB
Intel detailed the GPU D270 inference card: 480 GB LPDDR5X on partner cards, 160 GB on its own, 350 W PCIe. No price or ship date yet for buyers.
4 min read
- NewsModels & inference
Gemini 4 Argon reaches the Gemini API, AI Studio and the Gemini app
Gemini 4 Argon is now on the paid Gemini API at $2 input and $10 output per 1M tokens until November 30, with a 1M-token window and long decode.
4 min read