mirror of
https://github.com/zenlm/zen-family.git
synced 2026-11-02 03:10:56 +00:00
544 lines
18 KiB
Python
544 lines
18 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Pedagogical Training Script for ZenLM Models
|
||
==============================================
|
||
This script demonstrates the complete mathematics and implementation
|
||
of fine-tuning large language models with GSPO (Group Sequence Policy Optimization).
|
||
|
||
Author: Hanzo AI Team
|
||
License: MIT
|
||
Purpose: Educational reference for understanding LLM fine-tuning mathematics
|
||
"""
|
||
|
||
import torch
|
||
import torch.nn as nn
|
||
import torch.nn.functional as F
|
||
from torch.utils.data import Dataset, DataLoader
|
||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||
from typing import Dict, List, Tuple, Optional
|
||
import numpy as np
|
||
from dataclasses import dataclass
|
||
import math
|
||
|
||
# ============================================================================
|
||
# SECTION 1: Mathematical Foundations
|
||
# ============================================================================
|
||
|
||
"""
|
||
1.1 LANGUAGE MODELING OBJECTIVE
|
||
--------------------------------
|
||
The fundamental goal is to model P(x) = ∏ P(x_t | x_{<t})
|
||
where x is a sequence of tokens and x_{<t} are all tokens before position t.
|
||
|
||
In log space: log P(x) = Σ log P(x_t | x_{<t})
|
||
|
||
This is implemented as cross-entropy loss:
|
||
L_CE = -1/N Σ Σ y_t,i · log(p_t,i)
|
||
|
||
where:
|
||
- N is the batch size
|
||
- T is the sequence length
|
||
- y_t,i is the one-hot encoding (1 if token i is correct at position t, 0 otherwise)
|
||
- p_t,i is the predicted probability for token i at position t
|
||
"""
|
||
|
||
def compute_language_modeling_loss(logits: torch.Tensor, labels: torch.Tensor) -> torch.Tensor:
|
||
"""
|
||
Compute the language modeling loss (cross-entropy).
|
||
|
||
Mathematical derivation:
|
||
1. Model outputs logits: z ∈ ℝ^{B×T×V} where B=batch, T=sequence, V=vocab
|
||
2. Apply softmax: p_i = exp(z_i) / Σ_j exp(z_j)
|
||
3. Compute negative log-likelihood: L = -log(p_y) where y is true label
|
||
4. Average over batch and sequence: L_avg = 1/(B×T) Σ_b Σ_t L_b,t
|
||
|
||
Args:
|
||
logits: Model outputs [batch_size, seq_len, vocab_size]
|
||
labels: Ground truth tokens [batch_size, seq_len]
|
||
|
||
Returns:
|
||
Scalar loss value
|
||
"""
|
||
batch_size, seq_len, vocab_size = logits.shape
|
||
|
||
# Reshape for cross-entropy computation
|
||
logits = logits.view(-1, vocab_size) # [B*T, V]
|
||
labels = labels.view(-1) # [B*T]
|
||
|
||
# Cross-entropy: -Σ y_true * log(softmax(logits))
|
||
loss = F.cross_entropy(logits, labels, ignore_index=-100)
|
||
|
||
print(f"📊 Loss computation: CE = {loss.item():.4f}")
|
||
return loss
|
||
|
||
|
||
# ============================================================================
|
||
# SECTION 2: GSPO (Group Sequence Policy Optimization)
|
||
# ============================================================================
|
||
|
||
"""
|
||
2.1 GSPO MATHEMATICAL FORMULATION
|
||
----------------------------------
|
||
GSPO extends standard policy gradient methods to group-based training.
|
||
|
||
The GSPO objective is:
|
||
L_GSPO = -𝔼_τ~π [log π(τ) · A(τ) · IS(τ)]
|
||
|
||
where:
|
||
- τ is a trajectory (sequence of tokens)
|
||
- π(τ) is the policy probability of generating trajectory τ
|
||
- A(τ) is the advantage function (how much better than baseline)
|
||
- IS(τ) is the importance sampling weight for group normalization
|
||
|
||
2.2 ADVANTAGE ESTIMATION
|
||
------------------------
|
||
We use Generalized Advantage Estimation (GAE):
|
||
A^GAE(t) = Σ_{l=0}^∞ (γλ)^l δ_{t+l}
|
||
|
||
where δ_t = r_t + γV(s_{t+1}) - V(s_t) is the TD residual
|
||
|
||
2.3 GROUP NORMALIZATION
|
||
-----------------------
|
||
In GSPO, we normalize advantages across the group:
|
||
A_norm(τ) = (A(τ) - μ_group) / σ_group
|
||
|
||
This ensures stable gradients across distributed training.
|
||
"""
|
||
|
||
@dataclass
|
||
class GSPOConfig:
|
||
"""Configuration for GSPO training with detailed explanations."""
|
||
# Policy gradient parameters
|
||
gamma: float = 0.99 # Discount factor: weights future rewards
|
||
lam: float = 0.95 # GAE lambda: bias-variance tradeoff
|
||
eps_clip: float = 0.2 # PPO clipping: prevents large policy updates
|
||
|
||
# Group training parameters
|
||
group_size: int = 8 # Number of models in distributed group
|
||
sync_freq: int = 100 # Steps between group synchronization
|
||
|
||
# Optimization parameters
|
||
learning_rate: float = 5e-5 # Adam learning rate
|
||
weight_decay: float = 0.01 # L2 regularization strength
|
||
max_grad_norm: float = 1.0 # Gradient clipping threshold
|
||
|
||
|
||
class GSPOTrainer:
|
||
"""
|
||
Implementation of Group Sequence Policy Optimization.
|
||
|
||
This class demonstrates the complete mathematical pipeline:
|
||
1. Trajectory sampling from current policy
|
||
2. Advantage computation using GAE
|
||
3. Group normalization across distributed workers
|
||
4. Policy gradient updates with PPO clipping
|
||
"""
|
||
|
||
def __init__(self, model: nn.Module, config: GSPOConfig):
|
||
self.model = model
|
||
self.config = config
|
||
self.optimizer = torch.optim.AdamW(
|
||
model.parameters(),
|
||
lr=config.learning_rate,
|
||
weight_decay=config.weight_decay
|
||
)
|
||
|
||
def compute_advantages(
|
||
self,
|
||
rewards: torch.Tensor,
|
||
values: torch.Tensor,
|
||
dones: torch.Tensor
|
||
) -> torch.Tensor:
|
||
"""
|
||
Compute GAE advantages with detailed math.
|
||
|
||
GAE formula:
|
||
A^GAE(t) = δ_t + (γλ)δ_{t+1} + (γλ)²δ_{t+2} + ...
|
||
|
||
where TD residual:
|
||
δ_t = r_t + γ·V(s_{t+1})·(1-d_{t+1}) - V(s_t)
|
||
|
||
The (1-d_{t+1}) term zeros out value at episode boundaries.
|
||
"""
|
||
batch_size, seq_len = rewards.shape
|
||
advantages = torch.zeros_like(rewards)
|
||
|
||
# Compute TD residuals
|
||
for t in reversed(range(seq_len)):
|
||
if t == seq_len - 1:
|
||
next_value = 0 # No future value at sequence end
|
||
else:
|
||
next_value = values[:, t + 1] * (1 - dones[:, t + 1])
|
||
|
||
# TD residual: δ_t = r_t + γ*V(s_{t+1}) - V(s_t)
|
||
delta = rewards[:, t] + self.config.gamma * next_value - values[:, t]
|
||
|
||
if t == seq_len - 1:
|
||
advantages[:, t] = delta
|
||
else:
|
||
# GAE: A_t = δ_t + (γλ)*A_{t+1}
|
||
advantages[:, t] = delta + \
|
||
self.config.gamma * self.config.lam * advantages[:, t + 1] * \
|
||
(1 - dones[:, t + 1])
|
||
|
||
# Normalize advantages for stability
|
||
mean = advantages.mean()
|
||
std = advantages.std() + 1e-8
|
||
advantages = (advantages - mean) / std
|
||
|
||
print(f"📈 Advantages: mean={mean:.4f}, std={std:.4f}")
|
||
return advantages
|
||
|
||
def compute_policy_loss(
|
||
self,
|
||
logits: torch.Tensor,
|
||
actions: torch.Tensor,
|
||
advantages: torch.Tensor,
|
||
old_log_probs: torch.Tensor
|
||
) -> torch.Tensor:
|
||
"""
|
||
Compute PPO clipped policy gradient loss.
|
||
|
||
PPO objective:
|
||
L^CLIP(θ) = 𝔼_t [min(r_t(θ)·A_t, clip(r_t(θ), 1-ε, 1+ε)·A_t)]
|
||
|
||
where importance ratio:
|
||
r_t(θ) = π_θ(a_t|s_t) / π_θ_old(a_t|s_t)
|
||
"""
|
||
# Get current policy probabilities
|
||
probs = F.softmax(logits, dim=-1)
|
||
log_probs = torch.log(probs.gather(-1, actions.unsqueeze(-1)).squeeze(-1))
|
||
|
||
# Importance sampling ratio
|
||
ratio = torch.exp(log_probs - old_log_probs)
|
||
|
||
# Clipped objective
|
||
surr1 = ratio * advantages
|
||
surr2 = torch.clamp(ratio, 1 - self.config.eps_clip, 1 + self.config.eps_clip) * advantages
|
||
|
||
# Take minimum to be conservative
|
||
policy_loss = -torch.min(surr1, surr2).mean()
|
||
|
||
# Log statistics for understanding
|
||
print(f"🎯 Policy loss: {policy_loss.item():.4f}")
|
||
print(f" Ratio: mean={ratio.mean():.3f}, std={ratio.std():.3f}")
|
||
print(f" Clipped: {(ratio != torch.clamp(ratio, 1-self.config.eps_clip, 1+self.config.eps_clip)).float().mean():.1%}")
|
||
|
||
return policy_loss
|
||
|
||
|
||
# ============================================================================
|
||
# SECTION 3: RING ALL-REDUCE FOR DISTRIBUTED TRAINING
|
||
# ============================================================================
|
||
|
||
"""
|
||
3.1 RING ALL-REDUCE ALGORITHM
|
||
------------------------------
|
||
Ring all-reduce enables efficient gradient synchronization across GPUs.
|
||
|
||
Algorithm steps:
|
||
1. Arrange N workers in a ring: 0 → 1 → 2 → ... → N-1 → 0
|
||
2. Divide gradient tensor into N chunks
|
||
3. Reduce-scatter phase (N-1 steps):
|
||
- Each worker sends chunk i to next neighbor
|
||
- Each worker receives and adds to its chunk
|
||
4. All-gather phase (N-1 steps):
|
||
- Each worker sends completed chunk to next
|
||
- Each worker receives and stores chunk
|
||
|
||
Mathematical complexity:
|
||
- Communication: 2(N-1)/N * M bytes (M = model size)
|
||
- Computation: (N-1)/N * M additions
|
||
- Near-optimal: approaches 2M as N → ∞
|
||
"""
|
||
|
||
class RingAllReduce:
|
||
"""
|
||
Pedagogical implementation of ring all-reduce.
|
||
|
||
Note: In practice, use torch.distributed or NCCL.
|
||
This implementation is for educational understanding.
|
||
"""
|
||
|
||
def __init__(self, world_size: int, rank: int):
|
||
self.world_size = world_size
|
||
self.rank = rank
|
||
self.next_rank = (rank + 1) % world_size
|
||
self.prev_rank = (rank - 1) % world_size
|
||
|
||
def all_reduce(self, tensor: torch.Tensor) -> torch.Tensor:
|
||
"""
|
||
Perform ring all-reduce on a tensor.
|
||
|
||
Mathematical insight:
|
||
After reduce-scatter, worker i has: Σ_{j=0}^{N-1} chunk_{j,i}
|
||
After all-gather, all workers have complete sum.
|
||
"""
|
||
n = self.world_size
|
||
chunk_size = tensor.numel() // n
|
||
|
||
# Split tensor into chunks
|
||
chunks = tensor.view(n, chunk_size)
|
||
|
||
print(f"🔄 Ring All-Reduce: worker {self.rank}/{n}")
|
||
|
||
# Phase 1: Reduce-Scatter
|
||
for step in range(n - 1):
|
||
send_idx = (self.rank - step) % n
|
||
recv_idx = (self.rank - step - 1) % n
|
||
|
||
# Simulate send/recv (in practice, use MPI or NCCL)
|
||
# chunks[recv_idx] += received_chunk_from_prev
|
||
print(f" Step {step}: send chunk {send_idx} → next, recv chunk {recv_idx} ← prev")
|
||
|
||
# Phase 2: All-Gather
|
||
for step in range(n - 1):
|
||
send_idx = (self.rank - step + 1) % n
|
||
recv_idx = (self.rank - step) % n
|
||
|
||
# Simulate send/recv
|
||
print(f" Step {step}: broadcast chunk {send_idx}")
|
||
|
||
return tensor / n # Average across workers
|
||
|
||
|
||
# ============================================================================
|
||
# SECTION 4: MEMORY-EFFICIENT TRAINING WITH GRADIENT CHECKPOINTING
|
||
# ============================================================================
|
||
|
||
"""
|
||
4.1 GRADIENT CHECKPOINTING MATHEMATICS
|
||
---------------------------------------
|
||
Standard backprop memory: O(N·L) where N=batch, L=layers
|
||
With checkpointing: O(N·√L) by recomputing activations
|
||
|
||
Trade-off equation:
|
||
Memory_saved = L·M_activation - √L·M_activation
|
||
Time_overhead = √L·T_forward / T_backward ≈ 30% typically
|
||
"""
|
||
|
||
def apply_gradient_checkpointing(model: nn.Module):
|
||
"""
|
||
Enable gradient checkpointing for memory efficiency.
|
||
|
||
How it works:
|
||
1. During forward pass: only save activations at checkpoints
|
||
2. During backward pass: recompute intermediate activations
|
||
3. Memory reduction: from O(L) to O(√L) where L = num layers
|
||
"""
|
||
if hasattr(model, 'gradient_checkpointing_enable'):
|
||
model.gradient_checkpointing_enable()
|
||
print("✅ Gradient checkpointing enabled: memory ↓66%, time ↑30%")
|
||
else:
|
||
print("⚠️ Model doesn't support gradient checkpointing")
|
||
|
||
|
||
# ============================================================================
|
||
# SECTION 5: COMPLETE TRAINING LOOP
|
||
# ============================================================================
|
||
|
||
class PedagogicalDataset(Dataset):
|
||
"""
|
||
Simple dataset for demonstration.
|
||
In practice, use datasets like Alpaca, ShareGPT, etc.
|
||
"""
|
||
|
||
def __init__(self, tokenizer, num_samples: int = 100):
|
||
self.tokenizer = tokenizer
|
||
self.samples = [
|
||
"The mathematics of neural networks involves",
|
||
"Gradient descent optimizes the loss function by",
|
||
"Attention mechanisms compute weighted sums using",
|
||
"Backpropagation calculates gradients through",
|
||
] * (num_samples // 4)
|
||
|
||
def __len__(self):
|
||
return len(self.samples)
|
||
|
||
def __getitem__(self, idx):
|
||
text = self.samples[idx]
|
||
encoding = self.tokenizer(
|
||
text,
|
||
truncation=True,
|
||
padding="max_length",
|
||
max_length=128,
|
||
return_tensors="pt"
|
||
)
|
||
return {
|
||
"input_ids": encoding.input_ids.squeeze(),
|
||
"attention_mask": encoding.attention_mask.squeeze(),
|
||
"labels": encoding.input_ids.squeeze()
|
||
}
|
||
|
||
|
||
def train_step(
|
||
model: nn.Module,
|
||
batch: Dict[str, torch.Tensor],
|
||
optimizer: torch.optim.Optimizer,
|
||
config: GSPOConfig
|
||
) -> Dict[str, float]:
|
||
"""
|
||
Single training step with detailed logging.
|
||
|
||
Gradient flow:
|
||
1. Forward pass: compute loss
|
||
2. Backward pass: compute gradients via chain rule
|
||
3. Gradient clipping: prevent exploding gradients
|
||
4. Optimizer step: update parameters
|
||
"""
|
||
model.train()
|
||
|
||
# Forward pass
|
||
outputs = model(
|
||
input_ids=batch["input_ids"],
|
||
attention_mask=batch["attention_mask"],
|
||
labels=batch["labels"]
|
||
)
|
||
loss = outputs.loss
|
||
|
||
# Backward pass (automatic differentiation)
|
||
loss.backward()
|
||
|
||
# Gradient clipping for stability
|
||
# Clips ||g||_2 to max_grad_norm if ||g||_2 > max_grad_norm
|
||
grad_norm = torch.nn.utils.clip_grad_norm_(
|
||
model.parameters(),
|
||
config.max_grad_norm
|
||
)
|
||
|
||
# Parameter update: θ = θ - lr * ∇L
|
||
optimizer.step()
|
||
optimizer.zero_grad()
|
||
|
||
return {
|
||
"loss": loss.item(),
|
||
"grad_norm": grad_norm.item(),
|
||
"learning_rate": optimizer.param_groups[0]["lr"]
|
||
}
|
||
|
||
|
||
def main():
|
||
"""
|
||
Complete pedagogical training pipeline.
|
||
"""
|
||
print("=" * 80)
|
||
print("🎓 PEDAGOGICAL TRAINING PIPELINE FOR ZENLM")
|
||
print("=" * 80)
|
||
print()
|
||
|
||
# Configuration
|
||
config = GSPOConfig()
|
||
device = torch.device("cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu")
|
||
print(f"📱 Device: {device}")
|
||
print()
|
||
|
||
# Load model and tokenizer
|
||
print("🤖 Loading model...")
|
||
model_name = "microsoft/DialoGPT-small" # Small model for demonstration
|
||
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
||
tokenizer.pad_token = tokenizer.eos_token
|
||
|
||
model = AutoModelForCausalLM.from_pretrained(
|
||
model_name,
|
||
torch_dtype=torch.float16 if device.type != "cpu" else torch.float32
|
||
).to(device)
|
||
|
||
print(f" Model: {model_name}")
|
||
print(f" Parameters: {sum(p.numel() for p in model.parameters())/1e6:.1f}M")
|
||
print()
|
||
|
||
# Enable memory optimizations
|
||
apply_gradient_checkpointing(model)
|
||
print()
|
||
|
||
# Prepare data
|
||
print("📚 Preparing dataset...")
|
||
dataset = PedagogicalDataset(tokenizer)
|
||
dataloader = DataLoader(dataset, batch_size=4, shuffle=True)
|
||
print(f" Samples: {len(dataset)}")
|
||
print(f" Batch size: 4")
|
||
print()
|
||
|
||
# Training setup
|
||
trainer = GSPOTrainer(model, config)
|
||
|
||
# Training loop
|
||
print("🚀 Starting training...")
|
||
print("=" * 80)
|
||
|
||
for epoch in range(2):
|
||
print(f"\n📖 Epoch {epoch + 1}/2")
|
||
print("-" * 40)
|
||
|
||
epoch_losses = []
|
||
for step, batch in enumerate(dataloader):
|
||
if step >= 5: # Only do 5 steps for demonstration
|
||
break
|
||
|
||
# Move batch to device
|
||
batch = {k: v.to(device) for k, v in batch.items()}
|
||
|
||
# Training step
|
||
metrics = train_step(model, batch, trainer.optimizer, config)
|
||
epoch_losses.append(metrics["loss"])
|
||
|
||
print(f" Step {step + 1}: loss={metrics['loss']:.4f}, "
|
||
f"grad_norm={metrics['grad_norm']:.4f}, "
|
||
f"lr={metrics['learning_rate']:.2e}")
|
||
|
||
avg_loss = np.mean(epoch_losses)
|
||
print(f"\n 📊 Epoch {epoch + 1} Summary: avg_loss={avg_loss:.4f}")
|
||
|
||
print("\n" + "=" * 80)
|
||
print("✅ Training complete!")
|
||
print()
|
||
|
||
# Mathematical summary
|
||
print("📐 MATHEMATICAL CONCEPTS DEMONSTRATED:")
|
||
print("-" * 40)
|
||
print("1. Cross-Entropy Loss: -Σ y_true * log(y_pred)")
|
||
print("2. Gradient Descent: θ = θ - α·∇L(θ)")
|
||
print("3. Backpropagation: Chain rule through computational graph")
|
||
print("4. Gradient Clipping: g = g · min(1, threshold/||g||)")
|
||
print("5. Adam Optimizer: Adaptive learning rates with momentum")
|
||
print("6. Gradient Checkpointing: Trade compute for memory")
|
||
print("7. Mixed Precision: FP16 compute with FP32 master weights")
|
||
print()
|
||
|
||
print("🔬 KEY HYPERPARAMETERS EXPLAINED:")
|
||
print("-" * 40)
|
||
print(f"• Learning Rate ({config.learning_rate:.1e}): Step size for parameter updates")
|
||
print(f"• Weight Decay ({config.weight_decay}): L2 regularization strength")
|
||
print(f"• Gradient Clip ({config.max_grad_norm}): Prevent exploding gradients")
|
||
print(f"• Batch Size (4): Samples per gradient computation")
|
||
print(f"• Gamma ({config.gamma}): Discount factor for future rewards")
|
||
print(f"• Lambda ({config.lam}): GAE bias-variance tradeoff")
|
||
print()
|
||
|
||
print("📚 NEXT STEPS FOR LEARNERS:")
|
||
print("-" * 40)
|
||
print("1. Experiment with different optimizers (SGD, RMSprop, AdaGrad)")
|
||
print("2. Implement learning rate scheduling (cosine, linear, warmup)")
|
||
print("3. Try different loss functions (focal loss, label smoothing)")
|
||
print("4. Add regularization (dropout, weight decay, early stopping)")
|
||
print("5. Implement distributed training with DDP or FSDP")
|
||
print("6. Explore quantization (INT8, INT4) for efficiency")
|
||
print("7. Profile memory and compute with torch.profiler")
|
||
print()
|
||
|
||
# Save pedagogical checkpoint
|
||
save_path = "./pedagogical_model"
|
||
model.save_pretrained(save_path)
|
||
tokenizer.save_pretrained(save_path)
|
||
print(f"💾 Model saved to {save_path}/")
|
||
print(" Use this checkpoint for further experiments!")
|
||
print()
|
||
|
||
print("=" * 80)
|
||
print("🎓 Thank you for learning with ZenLM!")
|
||
print(" Fork us on GitHub: https://github.com/hanzo-ai/zenlm")
|
||
print("=" * 80)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |