mirror of
https://github.com/zenlm/zen-family.git
synced 2026-11-02 03:10:56 +00:00
390 lines
12 KiB
Python
390 lines
12 KiB
Python
"""
|
|
Training utilities for Zen models
|
|
Common functions for training setup, monitoring, and optimization
|
|
"""
|
|
|
|
import os
|
|
import torch
|
|
import wandb
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional, List, Tuple
|
|
from datetime import datetime
|
|
import numpy as np
|
|
from transformers import TrainerCallback, TrainerState, TrainerControl
|
|
import psutil
|
|
import GPUtil
|
|
|
|
|
|
class ZenTrainingMonitor(TrainerCallback):
|
|
"""Custom callback for monitoring Zen model training"""
|
|
|
|
def __init__(self, log_dir: str = "./logs", use_wandb: bool = False):
|
|
self.log_dir = Path(log_dir)
|
|
self.log_dir.mkdir(parents=True, exist_ok=True)
|
|
self.use_wandb = use_wandb
|
|
self.training_history = []
|
|
|
|
def on_log(self, args, state: TrainerState, control: TrainerControl, logs: Dict[str, Any], **kwargs):
|
|
"""Log training metrics"""
|
|
# Add timestamp
|
|
logs['timestamp'] = datetime.now().isoformat()
|
|
logs['step'] = state.global_step
|
|
logs['epoch'] = state.epoch
|
|
|
|
# Add system metrics
|
|
logs.update(self._get_system_metrics())
|
|
|
|
# Store in history
|
|
self.training_history.append(logs)
|
|
|
|
# Log to wandb if configured
|
|
if self.use_wandb:
|
|
wandb.log(logs)
|
|
|
|
# Save to file
|
|
log_file = self.log_dir / f"training_log_{datetime.now().strftime('%Y%m%d')}.jsonl"
|
|
with open(log_file, 'a') as f:
|
|
f.write(json.dumps(logs) + '\n')
|
|
|
|
def on_save(self, args, state: TrainerState, control: TrainerControl, **kwargs):
|
|
"""Save training history on checkpoint"""
|
|
history_file = Path(args.output_dir) / "training_history.json"
|
|
with open(history_file, 'w') as f:
|
|
json.dump(self.training_history, f, indent=2)
|
|
|
|
def _get_system_metrics(self) -> Dict[str, Any]:
|
|
"""Get system resource metrics"""
|
|
metrics = {}
|
|
|
|
# CPU metrics
|
|
metrics['cpu_percent'] = psutil.cpu_percent()
|
|
metrics['memory_percent'] = psutil.virtual_memory().percent
|
|
|
|
# GPU metrics
|
|
try:
|
|
gpus = GPUtil.getGPUs()
|
|
if gpus:
|
|
gpu = gpus[0] # Primary GPU
|
|
metrics['gpu_utilization'] = gpu.load * 100
|
|
metrics['gpu_memory_used'] = gpu.memoryUsed
|
|
metrics['gpu_memory_total'] = gpu.memoryTotal
|
|
metrics['gpu_temperature'] = gpu.temperature
|
|
except:
|
|
pass
|
|
|
|
return metrics
|
|
|
|
|
|
class OptimizationUtils:
|
|
"""Utilities for training optimization"""
|
|
|
|
@staticmethod
|
|
def setup_mixed_precision(fp16: bool = False, bf16: bool = False) -> Dict[str, Any]:
|
|
"""Setup mixed precision training"""
|
|
config = {}
|
|
|
|
if fp16:
|
|
config['fp16'] = True
|
|
config['half_precision_backend'] = 'auto'
|
|
elif bf16:
|
|
config['bf16'] = True
|
|
|
|
return config
|
|
|
|
@staticmethod
|
|
def calculate_gradient_accumulation(
|
|
batch_size: int,
|
|
target_batch_size: int,
|
|
world_size: int = 1
|
|
) -> int:
|
|
"""Calculate gradient accumulation steps"""
|
|
effective_batch_size = batch_size * world_size
|
|
accumulation_steps = max(1, target_batch_size // effective_batch_size)
|
|
return accumulation_steps
|
|
|
|
@staticmethod
|
|
def get_linear_schedule_with_warmup(
|
|
optimizer: torch.optim.Optimizer,
|
|
num_warmup_steps: int,
|
|
num_training_steps: int,
|
|
last_epoch: int = -1
|
|
):
|
|
"""Create linear schedule with warmup"""
|
|
from transformers import get_linear_schedule_with_warmup
|
|
return get_linear_schedule_with_warmup(
|
|
optimizer,
|
|
num_warmup_steps=num_warmup_steps,
|
|
num_training_steps=num_training_steps,
|
|
last_epoch=last_epoch
|
|
)
|
|
|
|
@staticmethod
|
|
def setup_distributed_training() -> Dict[str, Any]:
|
|
"""Setup distributed training configuration"""
|
|
config = {}
|
|
|
|
if torch.cuda.device_count() > 1:
|
|
config['ddp'] = True
|
|
config['ddp_find_unused_parameters'] = False
|
|
config['dataloader_num_workers'] = 4
|
|
config['local_rank'] = int(os.environ.get('LOCAL_RANK', -1))
|
|
|
|
return config
|
|
|
|
|
|
class CheckpointManager:
|
|
"""Manage model checkpoints"""
|
|
|
|
def __init__(self, checkpoint_dir: str, keep_last_n: int = 3):
|
|
self.checkpoint_dir = Path(checkpoint_dir)
|
|
self.checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
self.keep_last_n = keep_last_n
|
|
|
|
def save_checkpoint(
|
|
self,
|
|
model,
|
|
tokenizer,
|
|
optimizer,
|
|
scheduler,
|
|
epoch: int,
|
|
step: int,
|
|
metrics: Dict[str, Any]
|
|
) -> str:
|
|
"""Save training checkpoint"""
|
|
checkpoint_name = f"checkpoint-epoch{epoch}-step{step}"
|
|
checkpoint_path = self.checkpoint_dir / checkpoint_name
|
|
checkpoint_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Save model and tokenizer
|
|
model.save_pretrained(str(checkpoint_path))
|
|
tokenizer.save_pretrained(str(checkpoint_path))
|
|
|
|
# Save optimizer and scheduler
|
|
torch.save({
|
|
'optimizer_state_dict': optimizer.state_dict(),
|
|
'scheduler_state_dict': scheduler.state_dict() if scheduler else None,
|
|
'epoch': epoch,
|
|
'step': step,
|
|
'metrics': metrics
|
|
}, checkpoint_path / "training_state.pt")
|
|
|
|
# Save metrics
|
|
with open(checkpoint_path / "metrics.json", 'w') as f:
|
|
json.dump(metrics, f, indent=2)
|
|
|
|
# Cleanup old checkpoints
|
|
self._cleanup_old_checkpoints()
|
|
|
|
return str(checkpoint_path)
|
|
|
|
def load_checkpoint(
|
|
self,
|
|
checkpoint_path: str,
|
|
model,
|
|
optimizer=None,
|
|
scheduler=None
|
|
) -> Dict[str, Any]:
|
|
"""Load training checkpoint"""
|
|
checkpoint_path = Path(checkpoint_path)
|
|
|
|
# Load training state
|
|
training_state = torch.load(checkpoint_path / "training_state.pt")
|
|
|
|
if optimizer:
|
|
optimizer.load_state_dict(training_state['optimizer_state_dict'])
|
|
|
|
if scheduler and training_state.get('scheduler_state_dict'):
|
|
scheduler.load_state_dict(training_state['scheduler_state_dict'])
|
|
|
|
return {
|
|
'epoch': training_state['epoch'],
|
|
'step': training_state['step'],
|
|
'metrics': training_state['metrics']
|
|
}
|
|
|
|
def _cleanup_old_checkpoints(self):
|
|
"""Remove old checkpoints keeping only the last N"""
|
|
checkpoints = sorted(
|
|
[d for d in self.checkpoint_dir.iterdir() if d.is_dir() and d.name.startswith('checkpoint-')],
|
|
key=lambda x: x.stat().st_mtime,
|
|
reverse=True
|
|
)
|
|
|
|
for checkpoint in checkpoints[self.keep_last_n:]:
|
|
import shutil
|
|
shutil.rmtree(checkpoint)
|
|
|
|
|
|
def setup_training(
|
|
model_name: str,
|
|
training_stage: str,
|
|
output_dir: str,
|
|
use_wandb: bool = False,
|
|
project_name: str = "zen-training"
|
|
) -> Dict[str, Any]:
|
|
"""Setup training environment and configuration"""
|
|
|
|
# Create output directories
|
|
output_dir = Path(output_dir)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
log_dir = output_dir / "logs"
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
checkpoint_dir = output_dir / "checkpoints"
|
|
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Initialize wandb if configured
|
|
if use_wandb:
|
|
wandb.init(
|
|
project=project_name,
|
|
name=f"{model_name}-{training_stage}-{datetime.now().strftime('%Y%m%d-%H%M%S')}",
|
|
config={
|
|
"model": model_name,
|
|
"stage": training_stage,
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
)
|
|
|
|
# Setup configuration
|
|
config = {
|
|
"model_name": model_name,
|
|
"training_stage": training_stage,
|
|
"output_dir": str(output_dir),
|
|
"log_dir": str(log_dir),
|
|
"checkpoint_dir": str(checkpoint_dir),
|
|
"timestamp": datetime.now().isoformat(),
|
|
"use_wandb": use_wandb
|
|
}
|
|
|
|
# Add optimization settings
|
|
config.update(OptimizationUtils.setup_distributed_training())
|
|
|
|
# Save initial config
|
|
with open(output_dir / "training_config.json", "w") as f:
|
|
json.dump(config, f, indent=2)
|
|
|
|
return config
|
|
|
|
|
|
def calculate_training_metrics(
|
|
predictions: torch.Tensor,
|
|
labels: torch.Tensor
|
|
) -> Dict[str, float]:
|
|
"""Calculate training metrics"""
|
|
metrics = {}
|
|
|
|
# Calculate accuracy
|
|
if predictions.dim() > 1:
|
|
pred_labels = torch.argmax(predictions, dim=-1)
|
|
else:
|
|
pred_labels = predictions
|
|
|
|
correct = (pred_labels == labels).float()
|
|
metrics['accuracy'] = correct.mean().item()
|
|
|
|
# Calculate perplexity
|
|
if predictions.dim() > 2:
|
|
loss_fn = torch.nn.CrossEntropyLoss(reduction='mean')
|
|
loss = loss_fn(predictions.view(-1, predictions.size(-1)), labels.view(-1))
|
|
metrics['perplexity'] = torch.exp(loss).item()
|
|
|
|
return metrics
|
|
|
|
|
|
def save_training_summary(
|
|
output_dir: str,
|
|
model_name: str,
|
|
training_config: Dict[str, Any],
|
|
final_metrics: Dict[str, Any],
|
|
training_time: float
|
|
):
|
|
"""Save comprehensive training summary"""
|
|
|
|
summary = {
|
|
"model": model_name,
|
|
"configuration": training_config,
|
|
"final_metrics": final_metrics,
|
|
"training_time_hours": training_time / 3600,
|
|
"completed_at": datetime.now().isoformat()
|
|
}
|
|
|
|
summary_path = Path(output_dir) / "training_summary.json"
|
|
with open(summary_path, "w") as f:
|
|
json.dump(summary, f, indent=2)
|
|
|
|
# Also create a markdown summary
|
|
md_summary = f"""
|
|
# Training Summary: {model_name}
|
|
|
|
## Configuration
|
|
- **Model**: {model_name}
|
|
- **Stage**: {training_config.get('training_stage', 'unknown')}
|
|
- **Epochs**: {training_config.get('num_epochs', 'unknown')}
|
|
- **Batch Size**: {training_config.get('batch_size', 'unknown')}
|
|
- **Learning Rate**: {training_config.get('learning_rate', 'unknown')}
|
|
|
|
## Final Metrics
|
|
"""
|
|
|
|
for key, value in final_metrics.items():
|
|
md_summary += f"- **{key}**: {value:.4f}\n"
|
|
|
|
md_summary += f"\n## Training Time\n- **Total**: {training_time/3600:.2f} hours\n"
|
|
md_summary += f"\n## Completed\n- {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
|
|
|
md_path = Path(output_dir) / "README.md"
|
|
with open(md_path, "w") as f:
|
|
f.write(md_summary)
|
|
|
|
print(f"Training summary saved to {summary_path}")
|
|
|
|
|
|
def estimate_memory_usage(
|
|
model_size_gb: float,
|
|
batch_size: int,
|
|
sequence_length: int,
|
|
use_gradient_checkpointing: bool = False
|
|
) -> Dict[str, float]:
|
|
"""Estimate GPU memory usage"""
|
|
|
|
# Base model memory
|
|
model_memory = model_size_gb
|
|
|
|
# Gradient memory (roughly same as model)
|
|
gradient_memory = model_size_gb
|
|
|
|
# Optimizer states (AdamW uses 2x model size)
|
|
optimizer_memory = model_size_gb * 2
|
|
|
|
# Activation memory (rough estimate)
|
|
activation_memory = (batch_size * sequence_length * 4096 * 4) / (1024**3)
|
|
|
|
if use_gradient_checkpointing:
|
|
activation_memory *= 0.3 # Reduce by ~70% with checkpointing
|
|
|
|
total_memory = model_memory + gradient_memory + optimizer_memory + activation_memory
|
|
|
|
return {
|
|
"model_memory_gb": model_memory,
|
|
"gradient_memory_gb": gradient_memory,
|
|
"optimizer_memory_gb": optimizer_memory,
|
|
"activation_memory_gb": activation_memory,
|
|
"total_memory_gb": total_memory,
|
|
"recommended_gpu": _recommend_gpu(total_memory)
|
|
}
|
|
|
|
|
|
def _recommend_gpu(memory_gb: float) -> str:
|
|
"""Recommend GPU based on memory requirements"""
|
|
if memory_gb <= 16:
|
|
return "RTX 4070 (16GB) or better"
|
|
elif memory_gb <= 24:
|
|
return "RTX 4090 (24GB) or better"
|
|
elif memory_gb <= 40:
|
|
return "A100 40GB or better"
|
|
elif memory_gb <= 80:
|
|
return "A100 80GB or better"
|
|
else:
|
|
return "Multiple A100 80GB with model parallelism" |