Fine-tune LLMs on Custom Data 2026 — QLoRA Guide with HuggingFace

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Introduction

Why This Matters

Prompt engineering has limits. When you need consistent domain-specific behavior, a proprietary writing style, or dramatically reduced hallucinations on specialized topics, fine-tuning is the answer. A fine-tuned 7B model often beats a prompted 70B model on domain-specific tasks — while running 10x faster and costing 10x less per inference.

QLoRA (Quantized Low-Rank Adaptation) made fine-tuning accessible in 2023, and in 2026 it is the standard approach for teams that cannot afford to train from scratch. You can fine-tune a 7B model on a single 16GB GPU for under $5 on cloud GPUs, producing a model that is quantizably better than any system prompt for your specific task.

Understanding when to fine-tune versus when to use RAG or better prompting prevents expensive experiments that do not move the needle.

When to Fine-tune vs Prompt Engineering

SituationBest Solution
Need specific response formatPrompt engineering (cheaper, faster)
Specialized domain knowledgeFine-tuning
Consistent tone or personaFine-tuning
Very long system prompts (high latency)Fine-tune to "bake in" the behavior
Reduce hallucinations on your dataFine-tuning + RAG
Need 10x faster inferenceFine-tune a smaller model
New capability not in training dataFine-tuning or RAG

QLoRA: Fine-tune on Consumer Hardware

pip install transformers peft datasets trl bitsandbytes accelerate
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig
from datasets import Dataset
 
# 1. Load base model in 4-bit quantization
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)
 
model_name = "meta-llama/Llama-3.2-8B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
 
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map="auto",
)
model = prepare_model_for_kbit_training(model)
 
# 2. Configure LoRA adapters
lora_config = LoraConfig(
    r=16,                  # Rank: higher = more params, better quality
    lora_alpha=32,
    target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 4,194,304 || all params: 8,035,162,112 || trainable%: 0.052%

Only 0.05% of the model's parameters are updated during training — this is why QLoRA fits on a 16GB GPU.

Prepare Your Dataset

training_data = [
    {
        "instruction": "What is a Python list comprehension?",
        "response": "A list comprehension creates a new list by applying an expression to each item in an iterable.\n\n```python\nsquares = [x**2 for x in range(10)]\n```\n\nThis is equivalent to a for loop but more concise and faster."
    },
    # ... hundreds more examples
]
 
def format_instruction(sample: dict) -> str:
    return f"""<|begin_of_text|><|start_header_id|>system<|end_header_id|>
You are an expert Python tutor.<|eot_id|><|start_header_id|>user<|end_header_id|>
{sample['instruction']}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
{sample['response']}<|eot_id|>"""
 
dataset = Dataset.from_list(training_data)
dataset = dataset.map(lambda x: {"text": format_instruction(x)})

Training

training_args = SFTConfig(
    output_dir="./fine-tuned-model",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    fp16=True,
    logging_steps=10,
    save_strategy="epoch",
    warmup_ratio=0.03,
    lr_scheduler_type="cosine",
    max_seq_length=2048,
    report_to="none",
)
 
trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    args=training_args,
    dataset_text_field="text",
)
 
trainer.train()
trainer.save_model("./fine-tuned-model")

Inference with Fine-tuned Model

from peft import PeftModel
 
base_model = AutoModelForCausalLM.from_pretrained(
    model_name, quantization_config=bnb_config, device_map="auto"
)
model = PeftModel.from_pretrained(base_model, "./fine-tuned-model")
 
inputs = tokenizer(
    "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\nWhat is a decorator?<|eot_id|><|start_header_id|>assistant<|end_header_id|>",
    return_tensors="pt"
).to("cuda")
 
with torch.no_grad():
    outputs = model.generate(
        **inputs,
        max_new_tokens=300,
        temperature=0.1,
        do_sample=True,
        pad_token_id=tokenizer.eos_token_id,
    )
 
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

Deploy to HuggingFace Hub

# Merge LoRA weights into base model for deployment
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged-model")
tokenizer.save_pretrained("./merged-model")
 
from huggingface_hub import HfApi
api = HfApi()
api.upload_folder(
    folder_path="./merged-model",
    repo_id="your-username/my-python-tutor",
    repo_type="model"
)

Cost Estimate

GPUTime for 7B modelCost
Google Colab T4 (free)2-3 hoursFree
Google Colab A10030 min~$3
Runpod A100 80GB20 min~$2
Modal.com H10015 min~$5

Common Mistakes / Pitfalls

  • Using quantity over quality — 500 high-quality, diverse examples beat 5,000 repetitive ones
  • Mismatched chat template — always use the exact chat template of your base model
  • Training on the wrong format — instruction fine-tuning requires question/answer pairs, not raw text
  • No evaluation set — always hold out 10% of data to measure improvement, not just training loss
  • Fine-tuning when prompting would suffice — if a detailed system prompt achieves 90%, fine-tuning for 95% may not be worth the cost

Best Practices

  • Start with the model's official chat template format — deviating causes unpredictable behavior
  • Use a diverse dataset covering all your target use cases, including edge cases
  • Monitor both training loss and eval loss — training loss dropping while eval loss rises = overfitting
  • Run inference on your eval set manually after each epoch, not just automated metrics
  • Merge and upload LoRA weights to HuggingFace Hub immediately after training to prevent loss

Key Takeaways

  • QLoRA fine-tunes only 0.05% of model parameters, enabling 7B model training on a single 16GB GPU
  • A fine-tuned 7B model frequently outperforms a prompted 70B model on domain-specific tasks
  • Training 1,000 examples on an A100 GPU takes about 20 minutes and costs approximately $2-5
  • Chat template format must exactly match the base model — mismatches cause silently wrong outputs
  • LoRA adapters can be merged into the base model for deployment without adapter overhead at inference
  • Evaluation loss is a better training signal than training loss — monitor both during every run
  • HuggingFace Hub handles versioning, hosting, and inference endpoint deployment for fine-tuned models
  • Fine-tuning and RAG are complementary: RAG provides current facts, fine-tuning provides style and domain behavior

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading