Track A โ€” A Real Fine-Tune with Unsloth

Lesson 5: Track A โ€” A Real Fine-Tune with Unsloth

Unsloth is an open-source framework for training and running open-weight models. Its selling point is efficiency: it rewrites the attention and MLP kernels so that LoRA and QLoRA training runs substantially faster and in dramatically less VRAM than the standard Hugging Face stack, with no change to your data or your model choice. It also handles the fiddly parts โ€” chat templates, gradient checkpointing, quantisation โ€” with sane defaults.

Three ways to use it

ProductWhat it isPick it ifโ€ฆ
Unsloth CoreThe original Python library. pip install unsloth, then write the training script yourself (or paste a notebook).You want full control, scriptability, and reproducibility. This is the path this lesson teaches.
Unsloth StudioA local web UI: upload a dataset, pick a model, train, chat, export โ€” no code.You want a first result without writing Python, or you want their Data Recipes feature to generate a dataset.
Unsloth DesktopA packaged desktop app for Mac, Windows and Linux.You want the whole thing as an installed application.

Hardware reality check, straight from their docs: training works on NVIDIA, AMD, Intel and Apple Silicon (MLX); CUDA compute capability 7.0+ (V100, T4, RTX 20-series and newer) is the floor, and anything from 2018 onward generally works. CPU-only still works for chat and dataset generation. Python must be 3.11โ€“3.13, and CUDA 12.4+ is recommended (12.8+ for Blackwell).

You probably don't need to buy anything. Unsloth's own documentation states you can fine-tune for free on Google Colab or Kaggle, and that some workloads fit in as little as ~3 GB of VRAM. For a first project โ€” 200โ€“1,000 examples, a 3Bโ€“8B model, QLoRA, rank 16 โ€” a free Colab T4 (16 GB) is genuinely sufficient. Lesson 6 gives the exact VRAM per model size.

Install

# Recommended: uv with an explicit Python and automatic torch backend selection
uv venv unsloth_env --python 3.13
source unsloth_env/bin/activate
uv pip install unsloth --torch-backend=auto

# Or plain pip (unsloth pulls in matching torch / transformers / trl versions)
pip install unsloth

# Unsloth and vLLM together (for serving what you train)
uv pip install unsloth vllm --torch-backend=auto

# Linux, from scratch, if you need a venv first
apt install python3.13-venv -y
python -m venv unsloth_env
source unsloth_env/bin/activate
pip install --upgrade pip
pip install uv
uv pip install unsloth --torch-backend=auto

There is also an official Docker image (unsloth/unsloth on Docker Hub) which sidesteps every CUDA/driver version problem in one step.

The six steps of every fine-tune

Everything below is the same shape whether you are on a free Colab T4 or a rented H100. The numbers change; the pipeline does not.

Step 1

Load a base model in 4-bit

from unsloth import FastLanguageModel
import torch

max_seq_length = 2048   # context length; 2048 is the recommended test value
dtype = None            # None = auto; torch.bfloat16 on newer GPUs
load_in_4bit = True     # QLoRA. 4x less memory, ~1-2% accuracy cost.

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/llama-3.1-8b-unsloth-bnb-4bit",
    max_seq_length = max_seq_length,
    dtype = dtype,
    load_in_4bit = load_in_4bit,
)

Note the model name. Models ending in unsloth-bnb-4bit are Unsloth's dynamic 4-bit quants โ€” they use a little more VRAM than standard BitsAndBytes 4-bit but recover meaningfully more accuracy. Names ending in plain bnb-4bit are the ordinary variety, and names with no suffix are the full 16-bit originals. Use the Unsloth one when it exists. There is a full model catalogue and a "which model should I use" guide.

Step 2

Attach the LoRA adapters

model = FastLanguageModel.get_peft_model(
    model,
    r = 16,                       # rank: 8, 16, 32, 64 or 128
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj"],
    lora_alpha = 16,              # set equal to r, or 2x r
    lora_dropout = 0,             # 0 is the optimised path
    bias = "none",                # optimised path
    use_gradient_checkpointing = "unsloth",   # extra ~30% memory saving
    random_state = 3407,          # reproducible runs
    use_rslora = False,           # rank-stabilised LoRA (alpha/sqrt(r))
    loftq_config = None,
)

Unsloth's docs are unusually firm about target_modules: adapt all major linear layers, both attention and MLP. Removing modules to save memory costs quality for very little saving โ€” the RoUGE scores in their guide show all-layers winning over attention-only or MLP-only.

Step 3

Load your dataset

from datasets import load_dataset

dataset = load_dataset("json", data_files="train.jsonl", split="train")
# Each row: {"messages": [{"role": "user", ...}, {"role": "assistant", ...}]}
Step 4

Apply the model's chat template

from unsloth.chat_templates import get_chat_template

tokenizer = get_chat_template(tokenizer, chat_template="llama-3.1")

def formatting_prompts_func(examples):
    convos = examples["messages"]
    texts = [tokenizer.apply_chat_template(c, tokenize=False,
                                           add_generation_prompt=False)
             for c in convos]
    return {"text": texts}

dataset = dataset.map(formatting_prompts_func, batched=True)

This is the step that silently ruins runs when it is skipped or mismatched (Lesson 4). get_chat_template loads the correct special tokens for the family you chose โ€” llama-3.1, qwen-2.5, gemma-3, and so on. The chat templates guide lists the supported names.

Step 5

Train

from trl import SFTTrainer, SFTConfig

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,          # newer TRL builds call this processing_class
    train_dataset = dataset,
    args = SFTConfig(
        dataset_text_field = "text",
        max_seq_length = max_seq_length,
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 4,   # effective batch size = 8
        warmup_steps = 5,
        max_steps = 60,              # quick smoke test; use num_train_epochs for real runs
        learning_rate = 2e-4,
        logging_steps = 1,
        optim = "adamw_8bit",
        weight_decay = 0.01,
        lr_scheduler_type = "linear",
        seed = 3407,
        output_dir = "outputs",
        report_to = "none",
    ),
)
trainer.train()
Train on the answer, not the question. Masking the user turns so the loss is computed only on assistant tokens typically adds a percentage point or two of accuracy, and matters more on multi-turn data. Unsloth exposes it directly:

from unsloth.chat_templates import train_on_responses_only
trainer = train_on_responses_only(trainer, instruction_part="<|start_header_id|>user<|end_header_id|>\n\n", response_part="<|start_header_id|>assistant<|end_header_id|>\n\n")

For Gemma models the parts are "<start_of_turn>user\n" and "<start_of_turn>model\n". Get these strings wrong and you get a confusingly untrained model.
Step 6

Test, then export

FastLanguageModel.for_inference(model)   # Unsloth's 2x faster inference path

inputs = tokenizer([tokenizer.apply_chat_template(
    [{"role": "user", "content": "Claim: rear-ended at a stoplight."}],
    tokenize=False, add_generation_prompt=True)], return_tensors="pt").to("cuda")

out = model.generate(**inputs, max_new_tokens=256)
print(tokenizer.batch_decode(out)[0])

# Save the adapter (~tens to hundreds of MB)
model.save_pretrained("lora_adapter")
tokenizer.save_pretrained("lora_adapter")

# Or merge into a standalone model / export for local runtimes
model.save_pretrained_merged("merged_model", tokenizer, save_method="merged_16bit")
model.save_pretrained_gguf("gguf_model", tokenizer, quantization_method="q4_k_m")
The mistake to avoid: Unsloth's own guide calls it out explicitly โ€” do not jump straight to full fine-tuning (FFT) because LoRA "didn't work." If a task can't be learned with LoRA on a well-prepared dataset, the problem is almost always the data, the chat template, the learning rate, or the base model choice โ€” not the parameter-efficiency method. FFT will reproduce the same failure at 10ร— the cost. Diagnose first.

The Unsloth-specific gotchas worth knowing

GotchaDetail
tokenizer= vs processing_class=Recent TRL releases renamed the SFTTrainer argument. If you get an unexpected-keyword or deprecation error, swap to processing_class=tokenizer.
use_gradient_checkpointing="unsloth"Not a boolean โ€” the string is a Unsloth-specific implementation that cuts memory a further ~30% and enables very long context. True works but gives up the extra saving.
Training loss below ~0.2Per Unsloth's guide, that's the overfitting signal. Their cheap remedy: halve lora_alpha, or equivalently average the base model and the fine-tune's weights 50/50 โ€” a softer, less pronounced fine-tune.
Dynamic vs standard 4-bitDynamic (unsloth-bnb-4bit) costs slightly more VRAM and is worth it for accuracy.
Unsloth fixes gradient accumulationThey corrected the long-standing bug where b2/g8 and b8/g2 produced different loss curves despite equal effective batch size. In Unsloth, those configurations are now genuinely equivalent.
Verify the adapter actually changedDo not use np.allclose() to check that LoRA weights updated โ€” it misses the tiny Gaussian values in matrix A. Use a hash/checksum, the sum of absolute differences, or np.array_equal().
๐Ÿช™ Token angle: the free-tier path is not a toy. A 16 GB T4 with QLoRA fits an 8B model at rank 16, which covers the overwhelming majority of first fine-tuning projects. Reserve paid GPU hours for the run you have already proven works โ€” the difference between "one more free Colab iteration" and "an hour of A100 time" is the single biggest cost lever on this track, and it costs you nothing but patience.

๐Ÿ“บ Watch:

๐Ÿง  Knowledge Check

1. What does load_in_4bit = True give you, and what does it cost?

2. Why does Unsloth insist you adapt all seven linear modules rather than just attention?

3. Your LoRA run produced a model that behaves almost like the base model. What's the least likely explanation?

Further Reading