0306090120150180210240270300330km/h
0 km/h
ADIT JANGID

The Developer's Guide to Fine-Tuning Open Source Models

A practical guide to fine-tuning open-source LLMs using QLoRA, Hugging Face, and PyTorch. Learn how to train models on custom data formats.

The Developer's Guide to Fine-Tuning Open Source Models

Developers starting out with Large Language Models often face a critical question: Should I build a Retrieval-Augmented Generation (RAG) pipeline or should I fine-tune a model?

While RAG is excellent for giving your model access to dynamic knowledge, documents, and real-time facts, fine-tuning is the correct choice when you need to change the model's behavior, teach it a specific output syntax (like custom JSON schemas), align its tone, or train it on a highly specialized programming language.

Historically, fine-tuning was reserved for companies with large clusters of high-end GPUs. However, the introduction of parameter-efficient training methods has democratized the process.

In this guide, we will break down the mechanics of LoRA and QLoRA, outline dataset formats, and walk through a complete Python script using Hugging Face's ecosystem to train a model.


The Magic of QLoRA: Low-Rank Adaptation

Fine-tuning all parameters of a 7-billion parameter model (FP16) requires updating billions of numbers, demanding massive GPU memory because of gradient tracking.

LoRA (Low-Rank Adaptation) solves this by freezing the original weights of the model and injecting small trainable rank decomposition matrices (adapters) into the attention layers. This reduces the number of trainable parameters by up to 99%.

graph LR
    Input[Input Vector] --> Base[Frozen Base Weights W]
    Input --> AdapterA[Adapter A: Down Projection]
    AdapterA --> AdapterB[Adapter B: Up Projection]
    Base --> Combine[+]
    AdapterB --> Combine
    Combine --> Output[Output Vector]

QLoRA (Quantized LoRA) takes this a step further by quantizing the base model down to 4-bit precision using a specialized format called NormalFloat4 (NF4). It then trains the small LoRA adapters on top of this compressed model. This allows you to fine-tune a 7B or 8B model on a single consumer GPU with 16GB-24GB VRAM.


Structuring the Training Dataset

LLMs learn by example. The dataset must be structured as instruction-response pairs. Two standard formats are Alpaca format and ShareGPT format.

Alpaca Format (Simple Instruction-Response)

This format is perfect for single-turn Q&A tasks:

[
  {
    "instruction": "Convert the following temperature from Fahrenheit to Celsius.",
    "input": "98.6 F",
    "output": "To convert Fahrenheit to Celsius, subtract 32 from the temperature, multiply by 5, and divide by 9. Calculation: (98.6 - 32) * 5 / 9 = 37 C."
  }
]

ShareGPT Format (Conversational Multi-Turn)

Choose this format if you are building conversational chat agents:

[
  {
    "conversations": [
      { "from": "human", "value": "Hello, can you explain what an API is?" },
      { "from": "gpt", "value": "An API, or Application Programming Interface, acts as an intermediary..." },
      { "from": "human", "value": "Thanks! Can you give a quick example?" }
    ]
  }
]

Python Implementation: Fine-Tuning with QLoRA

Here is a complete, production-ready Python training script. It utilizes PyTorch, Transformers, Accelerate, PEFT, and TRL libraries to load a base model in 4-bit precision, construct LoRA layers, and execute Supervised Fine-Tuning (SFTTrainer).

Installing Dependencies

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install transformers peft trl accelerate bitsandbytes datasets

The Fine-Tuning Script (train.py)

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

def run_fine_tuning():
    model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
    output_dir = "./llama-3-8b-custom-adapter"
    
    # 1. Configure bitsandbytes for 4-bit quantization (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
    )
    
    # 2. Load model and tokenizer
    print("[*] Loading base model and tokenizer...")
    model = AutoModelForCausalLM.from_pretrained(
        model_id,
        quantization_config=bnb_config,
        device_map="auto"
    )
    model.config.use_cache = False  # Disable for training to save memory
    
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    tokenizer.pad_token = tokenizer.eos_token
    tokenizer.padding_side = "right"
    
    # 3. Prepare model for k-bit training
    model = prepare_model_for_kbit_training(model)
    
    # 4. Configure LoRA parameters
    # Target projection layers inside Llama's Self-Attention block
    peft_config = LoraConfig(
        r=16,                       # Rank: higher value allows learning more complex patterns
        lora_alpha=32,              # Scaling factor
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM"
    )
    
    model = get_peft_model(model, peft_config)
    print(f"[*] Trainable parameters: ")
    model.print_trainable_parameters()
    
    # 5. Load Dataset (Mocking standard Hugging Face dataset load)
    # Ensure your JSON files match the format required (e.g. keying off "text" or instructing SFTTrainer)
    print("[*] Loading dataset...")
    dataset = load_dataset("json", data_files={"train": "training_data.json"})
    
    # 6. Configure Training Arguments
    training_args = TrainingArguments(
        output_dir=output_dir,
        num_train_epochs=3,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4, # Simulates batch size of 16
        optim="paged_adamw_32bit",      # Optimizes memory layout
        save_steps=100,
        logging_steps=10,
        learning_rate=2e-4,            # standard initial rate for LoRA
        weight_decay=0.001,
        fp16=True,
        max_grad_norm=0.3,
        warmup_ratio=0.03,
        lr_scheduler_type="constant"
    )
    
    # 7. Initialize SFTTrainer
    trainer = SFTTrainer(
        model=model,
        train_dataset=dataset["train"],
        peft_config=peft_config,
        dataset_text_field="text",    # Expects instruction and response formatted together in a single "text" field
        max_seq_length=1024,
        tokenizer=tokenizer,
        args=training_args,
    )
    
    # 8. Start training loop
    print("[*] Launching training execution loop...")
    trainer.train()
    
    # 9. Save the adapter weights
    print(f"[*] Saving adapter weights to {output_dir}...")
    trainer.model.save_pretrained(output_dir)
    tokenizer.save_pretrained(output_dir)
    print("[✔] Fine-tuning complete!")

if __name__ == "__main__":
    run_fine_tuning()

Merging LoRA Adapters with Base Model

When training is complete, the SFTTrainer output directories will only contain the small weight changes (the adapter config and .safetensors files), not the full model weights.

To use the fine-tuned model inside inference servers like Ollama or vLLM, you must merge the adapter back into the base model:

from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

base_model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
adapter_dir = "./llama-3-8b-custom-adapter"

# Load the base model in FP16 (not quantized for merging)
base_model = AutoModelForCausalLM.from_pretrained(
    base_model_id,
    torch_dtype=torch.float16,
    device_map="cpu" # Run on CPU to avoid using up precious GPU VRAM during merge
)

# Load the adapter
model = PeftModel.from_pretrained(base_model, adapter_dir)

# Merge layers and save
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./llama-3-8b-merged-production")

Checklist for Fine-Tuning Execution

  1. Clean Dataset: 100 perfectly clean, formatted instructions will produce better results than 10,000 messy inputs containing HTML tags or syntax errors.
  2. GPU Monitoring: Keep a separate terminal window open with nvidia-smi -l 1 to monitor VRAM allocation. If you experience Out-Of-Memory (OOM) exceptions, reduce per_device_train_batch_size or scale down the max_seq_length.
  3. Loss Inspection: Watch the loss metric during log printouts. It should start around 2.0 - 2.5 and steadily decline to 0.5 - 1.0. If it drops to 0.1 immediately, your model is likely overfitting.
fine-tuningmachine learningqlorahuggingface

Related Posts

Interested in working together?

Let's translate complex software and automation problems into clean, high-performance systems.