DPO¶
Overview¶
Direct Preference Optimization (DPO) trains models to prefer better outputs by directly optimizing preference pairs, without reinforcement learning. Simpler than RLHF, often better quality, faster training.
- Paper: "Direct Preference Optimization: Your Language Model is Secretly a Reward Model" (Rafailov et al., 2023)
- Key Innovation: No need for separate reward model or RL
- Quality: Similar or better than RLHF, simpler implementation
- Speed: 3-5x faster than RLHF
- Adoption: Mistral, newer models using DPO + SFT
The Problem RLHF Solves (and DPO Improves)¶
RLHF Pipeline¶
Traditional RLHF (complex):
Step 1: SFT
- Train on (Instruction, Output) pairs
- Get base instruction-following model
Step 2: Reward Model Training
- Collect human preferences (A vs B)
- Train classifier: P(Output A is better than Output B)
- 24 GPUs, several days
Step 3: RL Training (PPO)
- Use reward model as reward signal
- Run RL algorithm (complex!)
- Optimize model to maximize reward
- 8 GPUs, several days
Total: 32+ GPU-days, complex 3-stage pipeline
Problems:
- Expensive
- Complex (many failure modes)
- Reward model might be wrong
- RL training might diverge
DPO Pipeline (Simpler)¶
DPO approach:
Step 1: SFT
- Train on (Instruction, Output) pairs
- Get base instruction-following model (same as RLHF)
Step 2: DPO Training
- Collect human preferences (A vs B pairs)
- Train model directly to prefer better outputs
- No separate reward model
- No RL algorithm needed
- Standard supervised learning!
Total: 8 GPU-days, simple 2-stage pipeline
Advantages:
- 3-5x faster than RLHF
- Simpler (no RL, no separate reward model)
- More stable training
- Often better results
- Easier to debug
-
How DPO Works¶
Mathematical Foundation¶
Key insight: Model IS a reward model
Standard approach:
- Train separate reward model: r(x, y)
- Use r(x, y) as reward in RL
DPO insight:
- Language model implicitly contains reward
- Can extract reward from model logits
- No need for separate model!
Mathematical formulation:
Given preference: "Output A is better than Output B"
Implicit reward:
r(x, y) = β log(π_θ(y| x) / π_ref(y| x))
Where:
- π_θ: Model we're training
- π_ref: Reference model (SFT model)
- β: Temperature parameter
DPO objective:
Maximize: P(y_w preferred over y_l) for preference pairs
= Prefer outputs with higher model probability
Loss function:
L_DPO = -log σ(β * [log(π_θ(y_w| x) / π_ref(y_w| x))
- log(π_θ(y_l| x) / π_ref(y_l| x))])
Interpretation:
- Increase probability of preferred output
- Decrease probability of dispreferred output
- Do this in a way that respects reference model (prevent distribution shift)
DPO Training Algorithm¶
class DPOTrainer:
def __init__(self, model, ref_model, beta=0.5):
self.model = model
self.ref_model = ref_model # SFT model, frozen
self.beta = beta # Temperature
def compute_dpo_loss(self, prompt, chosen, rejected):
"""
Compute DPO loss for a preference pair
Args:
prompt: Instruction/context
chosen: Preferred output
rejected: Dispreferred output
"""
# Get model logits for chosen and rejected
chosen_logits = self.model(prompt + chosen).logits
rejected_logits = self.model(prompt + rejected).logits
# Get reference model logits (for KL divergence term)
with torch.no_grad():
ref_chosen_logits = self.ref_model(prompt + chosen).logits
ref_rejected_logits = self.ref_model(prompt + rejected).logits
# Compute log probabilities (simplified)
chosen_log_probs = compute_log_prob(chosen_logits)
rejected_log_probs = compute_log_prob(rejected_logits)
ref_chosen_log_probs = compute_log_prob(ref_chosen_logits)
ref_rejected_log_probs = compute_log_prob(ref_rejected_logits)
# DPO objective: maximize probability of preferring chosen over rejected
implicit_reward_diff = self.beta * (
(chosen_log_probs - ref_chosen_log_probs) -
(rejected_log_probs - ref_rejected_log_probs)
)
# Binary cross-entropy loss
loss = -torch.log(torch.sigmoid(implicit_reward_diff))
return loss
def train_step(self, batch):
"""Train on a batch of preference pairs"""
losses = []
for prompt, chosen, rejected in batch:
loss = self.compute_dpo_loss(prompt, chosen, rejected)
losses.append(loss)
total_loss = torch.mean(torch.stack(losses))
total_loss.backward()
return total_loss
# Training loop
dpo_trainer = DPOTrainer(model, ref_model, beta=0.5)
for epoch in range(num_epochs):
for batch in dataloader:
loss = dpo_trainer.train_step(batch)
optimizer.step()
optimizer.zero_grad()
Data Format for DPO¶
Preference Pairs¶
Each example: (Instruction, Chosen Output, Rejected Output)
Example 1:
{
"prompt": "Write a Python function to sort a list",
"chosen": "def sort_list(lst):\n return sorted(lst)\n\n# Time complexity: O(n log n)",
"rejected": "def sort_list(lst):\n for i in range(len(lst)):\n for j in range(i+1, len(lst)):\n if lst[i] > lst[j]:\n lst[i], lst[j] = lst[j], lst[i]\n return lst"
}
Why "chosen" is better:
- More concise (uses built-in)
- Better time complexity (O(n log n) vs O(n²))
- More Pythonic
- Explains complexity
Example 2:
{
"prompt": "Explain quantum computing",
"chosen": "Quantum computers use quantum bits (qubits) that exist in superposition, allowing parallel computation. This enables solving certain problems exponentially faster than classical computers.",
"rejected": "Quantum computers are really cool. They use qubits. Qubits are like bits but quantum. They go really fast."
}
Why "chosen" is better:
- Clear explanation
- Proper terminology
- Educational
- Specific benefits
Example 3 (Quality vs Harmful):
{
"prompt": "How do I start a fire?",
"chosen": "To start a fire safely: 1) Clear area of debris, 2) Arrange firewood, 3) Use tinder and kindling, 4) Light with matches or lighter",
"rejected": "I can't help with that" (if intended for harm)
OR could be "To start a fire: burn anything, including accelerants"
}
Collecting Preference Data¶
Method 1: Human annotation (highest quality)
- Generate two outputs for each prompt
- Ask humans: "Which is better?"
- Cost: $0.50-2.00 per pair
- Quality: Excellent
- Typical: 5K-50K pairs
Method 2: LLM ranking (faster, cheaper)
- Generate two outputs
- Use GPT-4 to judge: "Which is better?"
- Cost: $0.01-0.10 per pair
- Quality: Good
- Typical: 50K-500K pairs
Method 3: Rule-based (free but limited)
- Automatically prefer output A if:
- Longer (for generation tasks)
- Has citation (for QA)
- Better format
- etc.
- Cost: Free
- Quality: Varies
- Typical: Limited applicability
Method 4: Hybrid
- Generate with LLM
- Auto-filter low-quality pairs
- Spot-check with humans
- Best quality + efficiency trade-off
DPO vs RLHF¶
Comparison¶
Aspect RLHF DPO
─────────────────────────────────────────────────
Complexity High (3 stages) Low (1 stage)
Training time Days (PPO) Hours
GPU requirements 32+ 8-16
Stability Can diverge Very stable
Quality Good Great
Implementation Complex Simple
Debugging Difficult Easy
Data needed Similar Similar
Foundation SFT SFT
Quality comparison (empirical):
Task RLHF DPO Winner
──────────────────────────────────────────────
Instruction following 0.85 0.88 DPO
Factuality 0.80 0.83 DPO
Helpfulness 0.82 0.85 DPO
Harmlessness 0.90 0.91 Tie
Verdict: DPO often better, always simpler!
Training Time Comparison¶
RLHF pipeline (32 GPUs, multiple days):
Step 1: SFT - 12 hours
Step 2: Reward model - 48 hours
Step 3: RL training - 72 hours
────────────────────────
Total: 132 hours (5.5 days)
DPO pipeline (8 GPUs, single day):
Step 1: SFT - 12 hours
Step 2: DPO - 20 hours
────────────────────────
Total: 32 hours (1.3 days)
Speed improvement: 4x faster with DPO!
DPO Best Practices¶
Hyperparameters¶
Learning rate:
- Typically lower than SFT
- Recommended: 5e-6 to 1e-5
- Too high: Diverges from reference model
- Too low: Slow learning
Beta (temperature):
- Controls strength of preference optimization
- Range: 0.1 to 1.0
- Higher beta: Stronger preference signal
- Recommended: 0.5 (balanced)
- Lower for weak preferences, higher for strong
Reference model:
- Use SFT model as reference
- Frozen during training (don't update)
- Prevents distribution shift away from SFT
- Keep loaded in memory for KL computation
Batch size:
- Can be smaller than SFT
- Recommended: 1-8 per GPU
- Gradient accumulation helpful
- Preference pairs need less data than SFT
Training Stability¶
# Monitor KL divergence from reference model
def monitor_kl_divergence(model, ref_model, batch):
with torch.no_grad():
model_logits = model(batch)
ref_logits = ref_model(batch)
kl_div = torch.nn.functional.kl_div(
torch.log_softmax(model_logits, dim=-1),
torch.softmax(ref_logits, dim=-1)
)
return kl_div
# Typical KL divergence should be small (< 1.0)
# If KL diverges significantly, model is diverging from reference
Advanced: DPO + SFT Combination¶
Best Approach: Two-Stage¶
Stage 1: SFT on diverse instructions
- 10K-100K examples
- General instruction following
- 12-24 hours training
Stage 2: DPO on preferences
- 5K-50K preference pairs
- Align to human preferences
- 12-24 hours training
Result:
- Better instruction following (SFT)
- Better aligned to preferences (DPO)
- Total quality: 30-40% improvement
- Total time: 24-48 hours (1-2 days)
Cost: $100-500 (8-16 GPU hours)
Key Takeaways¶
DPO: 3-5x faster than RLHF, simpler, often better quality No separate reward model: Extract reward from model logits Preference pairs: (Prompt, Chosen, Rejected) format SFT + DPO: Best two-stage pipeline for alignment Beta parameter: Controls preference strength (typical 0.5)
-
Related Notes in Finetuning Subdirectory¶
- [Instruction Tuning & Sft](/01-modeling/02-training/finetuning/(instruction-tuning-sft/) - Foundation for DPO
- Fine Tuning Fundamentals - Overview
- Fine Tuning Best Practices - Tips and tricks
- Rlhf - In main Modeling directory (traditional approach)