Academy

Fine-tune your first model on free Colab: QLoRA in about 40 lines

Fine-tuning a model doesn't have to need an expensive cluster. With QLoRA you can tune a small model on a free GPU in just a few dozen lines of code.

The Felesh team 4 min

Fine-tuning a language model used to require expensive compute clusters. Today, using a technique called QLoRA, you can fine-tune a small model on a free Colab GPU. This guide walks through the entire process in about forty lines of code.

Why QLoRA makes it possible

Fully fine-tuning a seven-billion-parameter model requires roughly 140 GB of memory — a job for several GPUs working in parallel. LoRA brings that figure down to about 16 GB. Instead of updating the entire model, it trains a few small adapter matrices and keeps the base weights frozen. QLoRA goes one step further: it quantises the base model to four-bit precision and trains those small matrices on top of it. For the same seven-billion-parameter model, the memory footprint drops to about 6 GB — easily fitting on an ordinary free GPU. The trade-off is minor: training is slightly slower and final quality marginally lower, which remains perfectly acceptable for most workloads.

Memory for one 7B fine-tune, three waysThree columns on one baseline. Full fine-tuning of a seven-billion-parameter model needs about 140 GB, with every weight trained. LoRA trains small adapters on frozen base weights and needs about 16 GB. QLoRA also stores the frozen base in four-bit precision and needs about 6 GB, which fits an ordinary free GPU.one 7B model, memory to fine-tuneabout 140 GBabout 16 GBabout 6 GBfull fine-tuneevery weight trainedLoRAbase frozenQLoRAbase in 4-bit
Full fine-tuning needs about 140 GB; QLoRA, with the frozen base in four bits, needs about 6 GB, which fits a free GPU.

The path, step by step

The implementation comes down to a few parts: load the base model in four-bit, attach the LoRA adapters, prepare the dataset, configure the training parameters, and run the trainer.

from transformers import (AutoModelForCausalLM, AutoTokenizer,
                          BitsAndBytesConfig, TrainingArguments)
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
from datasets import load_dataset
import torch

model_name = "your-base-model"

# 1. Load the base model in four-bit (QLoRA)
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
    model_name, quantization_config=bnb_config, device_map="auto")
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

# 2. Attach the LoRA adapters
lora_config = LoraConfig(
    r=16, lora_alpha=32, target_modules="all-linear",
    lora_dropout=0.05, bias="none", task_type="CAUSAL_LM")
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

# 3. Load the data (point this at your own dataset)
dataset = load_dataset("your_dataset", split="train")

# 4. Training settings
args = TrainingArguments(
    num_train_epochs=1,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=16,
    learning_rate=2e-4,
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    fp16=True,
    gradient_checkpointing=True,
    logging_steps=10,
    output_dir="./out")

# 5. Train and save
trainer = SFTTrainer(model=model, args=args,
                     train_dataset=dataset, max_seq_length=2048)
trainer.train()
model.save_pretrained("./my-lora-adapter")
Only the adapters learnThe loss on your data sends its gradient to matrix A and matrix B, both rank 16. Their product, scaled by alpha over r, which is 2, is the update. The base weights, held in four bits and set by load_in_4bit, are frozen and receive no gradient, so only A and B change.loss on your datamatrix Arank r = 16matrix Brank r = 16A · B × alpha / r = 2base weights · nf4 · frozenload_in_4bit=Trueno gradientonly A and B change; the base stays frozen
Only the small matrices A and B learn from the loss; the four-bit base underneath never receives a gradient.

The key settings to know

A few hyperparameters in this configuration determine the outcome. The rank (r) sets the size of the adapter matrices; a reliable starting point is r=16. The scaling factor (lora_alpha) is typically set to twice the rank, rendering 32 in this instance. Passing target_modules="all-linear" applies adapters across all linear layers, which has become standard practice for modern architectures. A learning rate of 2e-4 generally suits QLoRA fine-tuning. Finally, limit the number of epochs. A single epoch is often sufficient, and further training sharply increases the risk of overfitting the base model.

Merging and inference

After training, two deployment paths exist. You can keep the adapter separate — usually just a few dozen megabytes — and load it dynamically alongside the base model. This works well when you need several different adapters for different tasks on the same base model. Alternatively, you can merge the adapter weights directly into the base model. This yields a single, unified model that runs at native inference speed, which is typically preferred for static production deployments. To evaluate the result, set the model to evaluation mode, generate outputs against a sample validation prompt, and compare the completions against the base model.

Two deployment paths after trainingLeft: keep the adapter, a few dozen megabytes, separate from the base model, which loads adapter A and adapter B on the same base. Right: merge the adapter into the base, giving one model with native inference speed, with nothing extra to load, best for static production.keep the adapteradapter: a few dozen MBbase modeladapter Aadapter Bmerge into the baseone model, native speedbase + adapter weightsnothing extra to loadbest for static production
Keep the adapter to serve several tasks from one base, or merge it for one model that runs at native speed.

When you hit a problem

There are three common failure modes. If you run out of memory (OOM), apply these fixes in order: enable gradient checkpointing, drop the batch size to one while increasing gradient accumulation steps, ensure four-bit quantisation is active, and finally, truncate the sequence length. If the training loss fails to converge, slightly increase the learning rate and verify the adapters are correctly attached to the target modules. If the output degenerates into gibberish, the learning rate is likely too high, or the model has overfit due to excessive epochs. Tuning these specific parameters resolves the vast majority of initial fine-tuning hurdles.

Four fixes for out-of-memory, in orderThe run fails with CUDA out of memory. Then the fixes are tried one at a time: first gradient checkpointing on, then batch size 1 with gradient accumulation 16, then 4-bit loading kept with load_in_4bit set to True, and last a shorter max_seq_length. A memory bar under the list shrinks after each fix; its lengths are relative, not measured.torch.cuda.OutOfMemoryError: CUDA out of memory1234gradient checkpointing ongradient_checkpointing=Truebatch to 1, accumulation to 16per_device_train_batch_size=14-bit quantisation stays onload_in_4bit=Truetruncate the sequence lengthmax_seq_length: lower itGPU memory in use, relative, not to scaletried one at a time, top to bottom
Out of memory: try the four fixes in this order, and shorten the sequence length last.