|$ curl https://forge-ai.dev/api/markdown?path=docs/ai/fine-tuning
$cat docs/fine-tuning-models.md
updated Recentlyยท35 min readยทpublished

Fine-tuning Models

โ—†AIโ—†Advanced
Introduction

Fine-tuning adapts a pre-trained language model to a specific task or domain by continuing the training process on a curated dataset. Unlike prompting or RAG, fine-tuning modifies the model weights themselves, resulting in permanent behavioral changes that persist across all inputs.

Fine-tuning is the most effective way to imbue a model with deep domain knowledge, enforce complex formatting rules, or achieve consistent output style. However, it requires careful data curation, significant compute resources, and thorough evaluation to avoid catastrophic forgetting or overfitting.

This guide covers the full fine-tuning lifecycle: deciding when to fine-tune, preparing datasets, choosing between full fine-tuning and parameter-efficient methods, training with SFT, tuning hyperparameters, evaluating results, and deploying the final model.

When to Fine-Tune vs RAG vs Prompt Engineering

Choosing the right adaptation strategy depends on your use case, data availability, latency requirements, and desired behavior change.

ApproachBest ForCostWhen to Use
Prompt EngineeringSimple instruction followingFree โ€” no trainingQuick prototypes, well-defined tasks
RAGKnowledge-intensive tasksLow โ€” embedding + searchDynamic data, large corpora, factual accuracy
Fine-tuningBehavioral change, style, formatHigh โ€” GPU trainingConsistent output format, domain adaptation, tone
โœ“

best practice

Start with prompt engineering, add RAG for knowledge, and only fine-tune when you need the model to behave differently at a fundamental level. Fine-tuning changes the model's weights โ€” it is not a replacement for RAG or better prompting.
Full Fine-Tuning vs LoRA / QLoRA

Full fine-tuning updates all model parameters during training. It achieves the highest quality adaptation but is extremely expensive โ€” training a 7B parameter model requires ~60 GB of GPU memory per batch. Parameter-efficient methods reduce this dramatically.

MethodMemorySpeedQualityBest For
Full Fine-TuneVery highSlowHighestMaximum quality, large compute budget
LoRALowFastComparableMost practical scenarios
QLoRAVery lowModerateNear fullConsumer GPUs, large models (70B+)

LoRA (Low-Rank Adaptation)

LoRA freezes the pre-trained weights and injects trainable low-rank decomposition matrices into attention layers. This reduces trainable parameters by 10,000x while maintaining 90-95% of full fine-tuning quality. The LoRA weights are typically merged into the base model at inference for zero additional latency.

lora-config.py
Python
1from transformers import AutoModelForCausalLM, AutoTokenizer
2from peft import LoraConfig, get_peft_model
3
4model = AutoModelForCausalLM.from_pretrained(
5 "meta-llama/Llama-3.2-3B",
6 torch_dtype="auto",
7 device_map="auto"
8)
9
10lora_config = LoraConfig(
11 r=16,
12 lora_alpha=32,
13 target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
14 lora_dropout=0.05,
15 bias="none",
16 task_type="CAUSAL_LM"
17)
18
19model = get_peft_model(model, lora_config)
20print(f"Trainable params: {model.num_parameters(only_trainable=True):,}")

QLoRA โ€” Quantized LoRA

QLoRA extends LoRA by quantizing the base model to 4-bit using NormalFloat4 (NF4) quantization. This enables fine-tuning models like Llama 3.2 70B on a single 48 GB GPU. QLoRA introduces paged optimizers to handle gradient checkpointing memory spikes and double quantization to reduce the memory overhead of quantization constants.

qlora-config.py
Python
1from transformers import BitsAndBytesConfig
2import torch
3
4bnb_config = BitsAndBytesConfig(
5 load_in_4bit=True,
6 bnb_4bit_quant_type="nf4",
7 bnb_4bit_compute_dtype=torch.bfloat16,
8 bnb_4bit_use_double_quant=True
9)
10
11model = AutoModelForCausalLM.from_pretrained(
12 "meta-llama/Llama-3.2-70B",
13 quantization_config=bnb_config,
14 device_map="auto",
15 torch_dtype=torch.bfloat16
16)
17
18# Apply LoRA on top of quantized model
19model = get_peft_model(model, lora_config)
๐Ÿ”ฅ

pro tip

For most production use cases, start with LoRA (r=16, alpha=32). If you need to fine-tune models larger than 13B on consumer hardware, use QLoRA. Reserve full fine-tuning for cases where you have substantial compute and can quantify the quality improvement over LoRA.
Supervised Fine-Tuning (SFT)

Supervised Fine-Tuning (SFT) trains the model on input-output pairs to learn desired behavior. The training objective is standard autoregressive language modeling โ€” cross-entropy loss on the target tokens. SFT is the most common fine-tuning paradigm and forms the foundation for instruction-tuning, chat fine-tuning, and task-specific adaptation.

The training loop processes batches of examples where each example consists of a prompt (input) and a completion (target). The loss is computed only on the completion tokens, not the prompt tokens, preventing the model from being penalized for not predicting the input.

sft-training.py
Python
1from transformers import TrainingArguments, Trainer
2from datasets import Dataset
3
4training_args = TrainingArguments(
5 output_dir="./lora-fine-tuned",
6 per_device_train_batch_size=4,
7 gradient_accumulation_steps=4,
8 learning_rate=2e-4,
9 warmup_steps=100,
10 num_train_epochs=3,
11 logging_steps=25,
12 save_strategy="epoch",
13 evaluation_strategy="epoch",
14 bf16=True,
15 gradient_checkpointing=True,
16 remove_unused_columns=False,
17 report_to="wandb"
18)
19
20trainer = Trainer(
21 model=model,
22 args=training_args,
23 train_dataset=train_dataset,
24 eval_dataset=eval_dataset,
25 tokenizer=tokenizer,
26 data_collator=default_data_collator,
27)
28
29trainer.train()
30trainer.save_model()
โš 

warning

Never compute loss on prompt tokens. Apply a label masking strategy where prompt tokens are set to -100 in the labels tensor. The Hugging Face Trainer's data collator handles this if you format your dataset properly with a "labels" column that has -100 for non-target positions.

SFT Training Loop with Label Masking

sft-label-masking.py
Python
1import torch
2from transformers import AutoTokenizer
3
4tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.2-3B")
5tokenizer.pad_token = tokenizer.eos_token
6
7def format_sft_example(example):
8 prompt = example["instruction"]
9 response = example["output"]
10 messages = [
11 {"role": "user", "content": prompt},
12 {"role": "assistant", "content": response}
13 ]
14 formatted = tokenizer.apply_chat_template(
15 messages, tokenize=False
16 )
17 return {"text": formatted}
18
19def tokenize_with_mask(examples):
20 tokenized = tokenizer(
21 examples["text"],
22 truncation=True,
23 max_length=2048,
24 padding="max_length"
25 )
26
27 labels = tokenized["input_ids"].copy()
28 assistant_token_id = tokenizer.encode(
29 "assistant", add_special_tokens=False
30 )[0]
31
32 for i, input_ids in enumerate(tokenized["input_ids"]):
33 found_assistant = False
34 for j, tid in enumerate(input_ids):
35 if tid == assistant_token_id:
36 found_assistant = True
37 if not found_assistant:
38 labels[i][j] = -100
39
40 tokenized["labels"] = labels
41 return tokenized
Dataset Preparation

The quality of your fine-tuning dataset is the single most important factor in the success of fine-tuning. A small, high-quality dataset consistently outperforms a large, noisy one. The canonical recommendation is 500-5,000 high-quality examples for most SFT tasks.

Data Quality Criteria

โ—†Correctness โ€” responses must be factually accurate
โ—†Consistency โ€” similar inputs should have similar output formats
โ—†Diversity โ€” cover edge cases, different phrasings, varied complexity
โ—†No contradictions โ€” ensure consistent answers across similar questions
โ—†Appropriate difficulty โ€” match the complexity the model will see in production

ChatML Format

Most modern LLMs use chat templates (ChatML, ShareGPT, or model-specific formats) to structure multi-turn conversations. The tokenizer's apply_chat_template method handles this automatically.

chatml-format.py
Python
1# ChatML format (applied by tokenizer)
2messages = [
3 {"role": "system", "content": "You are a helpful assistant."},
4 {"role": "user", "content": "What is transfer learning?"},
5 {"role": "assistant", "content": "Transfer learning reuses a model trained on one task..."}
6]
7
8# Apply model-specific chat template
9formatted = tokenizer.apply_chat_template(
10 messages,
11 tokenize=False,
12 add_generation_prompt=False
13)
14print(formatted)
15# <|begin_of_text|><|start_header_id|>system<|end_header_id|>
16# You are a helpful assistant.<|eot_id|>
17# <|start_header_id|>user<|end_header_id|>
18# What is transfer learning?<|eot_id|>
19# <|start_header_id|>assistant<|end_header_id|>
20# Transfer learning reuses...<|eot_id|>
Hyperparameters

Fine-tuning hyperparameters significantly impact convergence speed, model quality, and training stability. The following table shows recommended starting values for LoRA fine-tuning of 1-13B parameter models.

HyperparameterRecommended ValueNotes
learning_rate1e-4 to 3e-4Higher than pre-training; LoRA needs higher LR
LoRA rank (r)8 to 32Higher rank = more expressive, more memory
LoRA alpha16 to 64Scaling factor; alpha = 2 * r is common
batch_size4-16 (effective)Use gradient accumulation for large effective batch
num_epochs2 to 5Watch for overfitting; eval per epoch
warmup_steps50 to 200Linear warmup; ~5-10% of total steps
weight_decay0.01 to 0.1Prevents overfitting on small datasets
max_seq_length512 to 2048Balance context needs with memory
โ„น

info

If training loss decreases but eval loss increases, you are overfitting. Reduce epochs, increase weight decay, or add dropout. If both losses plateau, increase learning rate or rank. Monitor both losses and stop training when eval loss plateaus.
Evaluation

Evaluating a fine-tuned model requires more than tracking loss curves. You need task-specific metrics, human evaluation, and comparison to the base model. Always evaluate on a held-out test set that was not seen during training.

eval-ft.py
Python
1from transformers import pipeline
2import evaluate
3
4# Load fine-tuned model
5pipe = pipeline(
6 "text-generation",
7 model="./lora-fine-tuned",
8 tokenizer=tokenizer,
9 device_map="auto"
10)
11
12# Compare base vs fine-tuned on test set
13for example in test_set[:5]:
14 base_output = base_pipe(example["prompt"], max_new_tokens=128)[0]["generated_text"]
15 ft_output = pipe(example["prompt"], max_new_tokens=128)[0]["generated_text"]
16 print(f"Prompt: {example['prompt']}")
17 print(f"Expected: {example['expected']}")
18 print(f"Base: {base_output}")
19 print(f"Fine-tuned: {ft_output}")
20 print("---")
โœ“

best practice

Always compare your fine-tuned model against the base model on the same test set. If the fine-tuned model does not show clear improvement on your target metrics, something is wrong โ€” check your dataset, hyperparameters, or consider if fine-tuning is the right approach.
Deployment

Fine-tuned models can be deployed via inference endpoints, serverless GPU functions, or embedded in applications. For LoRA adapters, you can merge weights into the base model for zero-overhead inference or keep them separate for modular switching.

deploy-ft.py
Python
1# Merge LoRA weights into base model for deployment
2from peft import PeftModel
3
4base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.2-3B")
5ft_model = PeftModel.from_pretrained(base_model, "./lora-fine-tuned")
6merged_model = ft_model.merge_and_unload()
7merged_model.save_pretrained("./merged-fine-tuned")
8
9# Or keep LoRA adapter separate for multi-adapter serving
10# Load different adapters for different tasks at inference time
11ft_model.load_adapter("./lora-adapter-1", adapter_name="code-gen")
12ft_model.load_adapter("./lora-adapter-2", adapter_name="chat")
13ft_model.set_adapter("code-gen")
โš 

warning

Merging LoRA weights into the base model increases model size by the rank dimension but eliminates the need for the PEFT library at inference. For production serving with multiple adapters, keep adapters separate and use adapter routing. Test inference latency after merging โ€” some merge operations change model precision.
$Blueprint โ€” Engineering DocumentationยทSection ID: AI-FT-01ยทRevision: 1.0