Skip to content
HUIAHUB

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.

Edited by Published 6 min read15 sources

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.

SymptomTry firstWhy
The model does not know your product, docs or recent eventsRetrieval (RAG)Facts change; retrieval updates without retraining and can show sources
Answers need citationsRetrieval (RAG)A fine-tuned model has no record of where a fact came from
The model ignores a format you described onceA better prompt with examples or structured outputCheap to try and easy to undo
Your prompt has grown to dozens of examples and still driftsFine-tuningThe 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 wellFine-tuning a small modelOutputs from the large model can become the training set
You can show the behavior in examples but cannot describe it in rulesFine-tuningSupervised training learns from pairs, not instructions
You have fewer than a handful of good examplesPromptingThere 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 r setting sets the size of the update matrices. A higher rank means more trainable parameters and more capacity. The PEFT default is r=8.
  • Scaling. The update is scaled by lora_alpha/r, or by lora_alpha/math.sqrt(r) with use_rslora=True. The default lora_alpha is 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:

  1. Every completion is an answer you would accept in production.
  2. The label set is balanced enough that the model cannot score well by always guessing one class.
  3. No test prompt appears, even reworded, in the training file.
  4. 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-4 follows TRL's tip for adapters. The SFTConfig default is 2e-5, which suits full fine-tuning.
  • model_init_kwargs sets the load precision. Without a dtype, TRL loads the model in float32.
  • bf16 defaults to True when fp16 is not set. On a GPU without bfloat16 support, set fp16=True and load in torch.float16 instead.
  • gradient_checkpointing defaults to True in SFTConfig, 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. 1.PEFT · Hugging FaceOfficial docsPrimaryhuggingface.co
  2. 2.LoRA · Hugging FaceOfficial docsPrimaryhuggingface.co
  3. 3.SFT Trainer · Hugging FaceOfficial docsPrimaryhuggingface.co
  4. 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. 5.huggingface.co/docs/trl/unsloth_integrationRepositoryPrimary
  6. 6.huggingface.co/docs/peft/developer_guides/loraRepositoryPrimary
  7. 7.huggingface.co/docs/peft/package_reference/peft_modelRepositoryPrimary
  8. 8.huggingface.co/docs/peft/quicktourRepositoryPrimary
  9. 9.huggingface.co/docs/peft/installRepositoryPrimary
  10. 10.huggingface.co/docs/trl/installationRepositoryPrimary
  11. 11.huggingface.co/docs/datasets/installationRepositoryPrimary
  12. 12.huggingface.co/docs/datasets/loadingRepositoryPrimary
  13. 13.huggingface.co/docs/transformers/chat_templatingRepositoryPrimary
  14. 14.arxiv.org/abs/2106.09685PaperPrimary
  15. 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