ring_allreduce() contained its own simulation that multiplied each gradient value by world_size (to fake an AllReduce sum), bypassing the ProcessGroup dispatch entirely. This meant the overlapped synchronization path never used NCCL or RNCCL, even when those features were compiled in. Replace the hand-rolled simulation with a call to self.process_group.allreduce(tensor, ReduceOp::Sum) so the overlapped path uses the same backend as synchronize_gradients_sequential. The communication latency sleep is kept for benchmarking purposes. Add test_ring_allreduce_matches_sequential_path to verify both paths produce identical gradient values under CPU simulation. Closes #10
847 lines
28 KiB
Rust
847 lines
28 KiB
Rust
//! Multi-GPU trainer implementation for distributed training
|
|
//!
|
|
//! This module provides the core MultiGpuTrainer implementation that coordinates
|
|
//! training across multiple GPUs with near-linear scaling efficiency.
|
|
|
|
use crate::Backend;
|
|
use crate::comm::{CommunicationPrimitive, ReduceOp};
|
|
use crate::error::{DistributedError, Result};
|
|
use crate::group::ProcessGroup;
|
|
use rtx_tensor::{Device, Tensor};
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::sync::RwLock;
|
|
use tracing::{debug, info, warn};
|
|
|
|
/// Multi-GPU trainer for coordinating distributed training
|
|
#[derive(Debug)]
|
|
pub struct MultiGpuTrainer {
|
|
/// Local GPU rank (0 to world_size-1)
|
|
pub local_rank: usize,
|
|
/// Total number of GPUs across all nodes
|
|
pub world_size: usize,
|
|
/// Process group for communication
|
|
pub process_group: ProcessGroup,
|
|
/// Available GPU devices
|
|
pub devices: Vec<Device>,
|
|
/// Training metrics
|
|
pub metrics: Arc<RwLock<TrainingMetrics>>,
|
|
/// Load balancer for work distribution
|
|
pub load_balancer: LoadBalancer,
|
|
/// Fault tolerance manager
|
|
pub fault_tolerance: FaultTolerance,
|
|
/// Scaling optimizer for communication overlap
|
|
pub scaling_optimizer: ScalingOptimizer,
|
|
/// Dynamic loss scaling for mixed precision training
|
|
pub loss_scaler: Arc<RwLock<DynamicLossScaler>>,
|
|
}
|
|
|
|
/// Training metrics for performance monitoring
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct TrainingMetrics {
|
|
/// Time spent synchronizing gradients (milliseconds)
|
|
pub gradient_sync_time_ms: f64,
|
|
/// Communication overhead as percentage of total time
|
|
pub communication_overhead_percent: f64,
|
|
/// Memory imbalance ratio across GPUs (0.0 = perfect balance)
|
|
pub memory_imbalance_ratio: f64,
|
|
/// Scaling efficiency (1.0 = linear scaling)
|
|
pub scaling_efficiency: f64,
|
|
/// Time to recover from GPU failure (milliseconds)
|
|
pub fault_recovery_time_ms: f64,
|
|
/// Total training steps completed
|
|
pub training_steps: u64,
|
|
/// Average throughput (samples per second)
|
|
pub throughput_samples_per_sec: f64,
|
|
}
|
|
|
|
/// Load balancer for optimal work distribution across GPUs
|
|
#[derive(Debug)]
|
|
pub struct LoadBalancer {
|
|
/// Current GPU utilization percentages (0.0 to 1.0)
|
|
pub gpu_utilizations: Vec<f64>,
|
|
/// Memory usage per GPU in bytes
|
|
pub memory_usage: Vec<u64>,
|
|
/// Maximum allowed memory imbalance (0.1 = 10%)
|
|
pub imbalance_threshold: f64,
|
|
/// Work distribution weights per GPU
|
|
pub distribution_weights: Vec<f64>,
|
|
}
|
|
|
|
/// Fault tolerance manager for handling GPU failures
|
|
#[derive(Debug)]
|
|
pub struct FaultTolerance {
|
|
/// Health status of each GPU (true = healthy)
|
|
pub health_monitors: Vec<bool>,
|
|
/// Maximum time allowed for recovery
|
|
pub recovery_timeout: Duration,
|
|
/// List of failed GPU ranks
|
|
pub failed_gpus: Vec<usize>,
|
|
/// Checkpoint interval for fault recovery
|
|
pub checkpoint_interval: Duration,
|
|
/// Last checkpoint timestamp
|
|
pub last_checkpoint: Instant,
|
|
}
|
|
|
|
/// Scaling optimizer for communication/computation overlap
|
|
#[derive(Debug)]
|
|
pub struct ScalingOptimizer {
|
|
/// Whether communication overlap is enabled
|
|
pub communication_overlap_enabled: bool,
|
|
/// Number of CUDA streams for communication
|
|
pub communication_streams: usize,
|
|
/// Number of CUDA streams for computation
|
|
pub compute_streams: usize,
|
|
/// Ring all-reduce buffer size
|
|
pub ring_buffer_size: usize,
|
|
/// Gradient compression enabled
|
|
pub gradient_compression: bool,
|
|
}
|
|
|
|
/// Dynamic loss scaler for mixed precision training
|
|
#[derive(Debug)]
|
|
pub struct DynamicLossScaler {
|
|
/// Current loss scale factor
|
|
pub scale: f32,
|
|
/// Growth factor for increasing scale
|
|
pub growth_factor: f32,
|
|
/// Backoff factor for decreasing scale
|
|
pub backoff_factor: f32,
|
|
/// Number of steps without overflow
|
|
pub steps_without_overflow: usize,
|
|
/// Steps required before growing scale
|
|
pub growth_interval: usize,
|
|
/// Minimum allowed scale
|
|
pub min_scale: f32,
|
|
/// Maximum allowed scale
|
|
pub max_scale: f32,
|
|
/// Current mixed precision mode
|
|
pub precision_mode: String,
|
|
/// Whether to check for overflow
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl MultiGpuTrainer {
|
|
/// Create a new multi-GPU trainer
|
|
pub async fn new(world_size: usize, local_rank: usize) -> Result<Self> {
|
|
info!(
|
|
"Initializing MultiGpuTrainer with world_size={}, local_rank={}",
|
|
world_size, local_rank
|
|
);
|
|
|
|
// Validate input parameters
|
|
if world_size == 0 {
|
|
return Err(DistributedError::configuration("world_size must be > 0"));
|
|
}
|
|
if local_rank >= world_size {
|
|
return Err(DistributedError::configuration(format!(
|
|
"local_rank ({local_rank}) must be < world_size ({world_size})"
|
|
)));
|
|
}
|
|
|
|
// Initialize process group with NCCL backend
|
|
use crate::group::WorldInfo;
|
|
let world_info = WorldInfo::new(world_size as i32, local_rank as i32, Backend::Nccl);
|
|
let process_group = ProcessGroup::new(Backend::Nccl, world_info)?;
|
|
|
|
// Create GPU devices
|
|
let devices = (0..world_size).map(Device::Cuda).collect();
|
|
|
|
// Initialize load balancer
|
|
let load_balancer = LoadBalancer::new(world_size);
|
|
|
|
// Initialize fault tolerance
|
|
let fault_tolerance = FaultTolerance::new(world_size);
|
|
|
|
// Initialize scaling optimizer
|
|
let scaling_optimizer = ScalingOptimizer::new();
|
|
|
|
// Initialize dynamic loss scaler
|
|
let loss_scaler = DynamicLossScaler::new();
|
|
|
|
let trainer = Self {
|
|
local_rank,
|
|
world_size,
|
|
process_group,
|
|
devices,
|
|
metrics: Arc::new(RwLock::new(TrainingMetrics::default())),
|
|
load_balancer,
|
|
fault_tolerance,
|
|
scaling_optimizer,
|
|
loss_scaler: Arc::new(RwLock::new(loss_scaler)),
|
|
};
|
|
|
|
info!("MultiGpuTrainer initialized successfully");
|
|
Ok(trainer)
|
|
}
|
|
|
|
/// Synchronize gradients across all GPUs using ring all-reduce
|
|
pub async fn synchronize_gradients(&mut self, gradients: &mut [Tensor]) -> Result<()> {
|
|
let start_time = Instant::now();
|
|
debug!(
|
|
"Starting gradient synchronization across {} GPUs",
|
|
self.world_size
|
|
);
|
|
|
|
// Check for failed GPUs before synchronization
|
|
self.fault_tolerance.check_gpu_health().await?;
|
|
|
|
if self.scaling_optimizer.communication_overlap_enabled {
|
|
self.synchronize_gradients_overlapped(gradients).await?;
|
|
} else {
|
|
self.synchronize_gradients_sequential(gradients).await?;
|
|
}
|
|
|
|
// Update metrics
|
|
let sync_time = start_time.elapsed().as_millis() as f64;
|
|
{
|
|
let mut metrics = self.metrics.write().await;
|
|
metrics.gradient_sync_time_ms = sync_time;
|
|
}
|
|
|
|
debug!("Gradient synchronization completed in {:.2}ms", sync_time);
|
|
Ok(())
|
|
}
|
|
|
|
/// Sequential gradient synchronization (no overlap)
|
|
async fn synchronize_gradients_sequential(&mut self, gradients: &mut [Tensor]) -> Result<()> {
|
|
let num_gradients = gradients.len();
|
|
for (i, gradient) in gradients.iter_mut().enumerate() {
|
|
debug!(
|
|
"Synchronizing gradient tensor {} of {}",
|
|
i + 1,
|
|
num_gradients
|
|
);
|
|
|
|
// Perform all-reduce to sum gradients across all GPUs
|
|
self.process_group
|
|
.allreduce(gradient, ReduceOp::Sum)
|
|
.await?;
|
|
|
|
// Average the gradients by dividing by world size
|
|
let world_size = self.world_size as f32;
|
|
*gradient = gradient.div_scalar(world_size)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Overlapped gradient synchronization (communication + computation)
|
|
async fn synchronize_gradients_overlapped(&mut self, gradients: &mut [Tensor]) -> Result<()> {
|
|
info!("Using ring AllReduce with communication overlap");
|
|
|
|
let world_size = self.world_size;
|
|
let rank = self.local_rank;
|
|
let buffer_size = self.scaling_optimizer.ring_buffer_size;
|
|
let gradient_count = gradients.len();
|
|
|
|
for (i, gradient) in gradients.iter_mut().enumerate() {
|
|
debug!(
|
|
"Ring AllReduce for gradient tensor {} of {}",
|
|
i + 1,
|
|
gradient_count
|
|
);
|
|
|
|
// Apply gradient compression if enabled
|
|
if self.scaling_optimizer.gradient_compression {
|
|
self.apply_gradient_compression(gradient).await?;
|
|
}
|
|
|
|
// Perform ring AllReduce algorithm
|
|
self.ring_allreduce(gradient, rank, world_size, buffer_size)
|
|
.await?;
|
|
|
|
// Average the gradients
|
|
let world_size_f32 = world_size as f32;
|
|
*gradient = gradient.div_scalar(world_size_f32)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Ring AllReduce implementation with communication overlap
|
|
async fn ring_allreduce(
|
|
&mut self,
|
|
tensor: &mut Tensor,
|
|
_rank: usize,
|
|
world_size: usize,
|
|
buffer_size: usize,
|
|
) -> Result<()> {
|
|
let tensor_data = tensor.data()?;
|
|
let total_elements = tensor_data.len();
|
|
|
|
if total_elements == 0 {
|
|
return Ok(());
|
|
}
|
|
|
|
let chunk_size = total_elements.div_ceil(world_size);
|
|
debug!(
|
|
"Ring AllReduce: tensor size={}, chunk_size={}, world_size={}",
|
|
total_elements, chunk_size, world_size
|
|
);
|
|
|
|
// Delegate to the process group for the actual reduction.
|
|
// This dispatches to NCCL or RNCCL when the corresponding feature is compiled in,
|
|
// ensuring the overlapped path uses the same communication backend as the
|
|
// sequential path (synchronize_gradients_sequential). Without this delegation
|
|
// the ring simulation bypassed NCCL even when it was available.
|
|
self.process_group.allreduce(tensor, ReduceOp::Sum).await?;
|
|
|
|
// Simulate ring communication latency for benchmarking purposes
|
|
let total_comm_time_us = (total_elements as f64 / buffer_size as f64 * 20.0) as u64;
|
|
tokio::time::sleep(Duration::from_micros(total_comm_time_us.max(10))).await;
|
|
|
|
debug!("Ring AllReduce completed successfully");
|
|
Ok(())
|
|
}
|
|
|
|
/// Apply gradient compression if enabled
|
|
async fn apply_gradient_compression(&self, gradient: &mut Tensor) -> Result<()> {
|
|
if !self.scaling_optimizer.gradient_compression {
|
|
return Ok(());
|
|
}
|
|
|
|
debug!("Applying gradient compression");
|
|
|
|
// Simple gradient compression: quantize to reduce precision
|
|
let mut data = gradient.data()?;
|
|
|
|
// Apply simple quantization (reduce precision)
|
|
for value in &mut data {
|
|
// Quantize to reduce bandwidth (simple approach)
|
|
*value = (*value * 1000.0).round() / 1000.0;
|
|
}
|
|
|
|
*gradient = Tensor::from_data(data, gradient.shape().dims().to_vec(), gradient.device())?;
|
|
|
|
// Simulate compression overhead
|
|
tokio::time::sleep(Duration::from_micros(5)).await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Measure scaling efficiency with current configuration
|
|
pub async fn measure_scaling_efficiency(&mut self, target_gpus: usize) -> Result<f64> {
|
|
info!("Measuring scaling efficiency for {} GPUs", target_gpus);
|
|
|
|
if target_gpus > self.world_size {
|
|
return Err(DistributedError::configuration(format!(
|
|
"Cannot measure {} GPU efficiency with only {} GPUs available",
|
|
target_gpus, self.world_size
|
|
)));
|
|
}
|
|
|
|
// Benchmark single GPU performance
|
|
let single_gpu_throughput = self.benchmark_single_gpu_throughput().await?;
|
|
debug!(
|
|
"Single GPU throughput: {:.2} samples/sec",
|
|
single_gpu_throughput
|
|
);
|
|
|
|
// Benchmark multi-GPU performance
|
|
let multi_gpu_throughput = self.benchmark_multi_gpu_throughput(target_gpus).await?;
|
|
debug!(
|
|
"Multi GPU ({}) throughput: {:.2} samples/sec",
|
|
target_gpus, multi_gpu_throughput
|
|
);
|
|
|
|
// Calculate efficiency: actual_speedup / ideal_speedup
|
|
let ideal_throughput = single_gpu_throughput * target_gpus as f64;
|
|
let efficiency = if ideal_throughput > 0.0 {
|
|
(multi_gpu_throughput / ideal_throughput) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Update metrics
|
|
{
|
|
let mut metrics = self.metrics.write().await;
|
|
metrics.scaling_efficiency = efficiency;
|
|
}
|
|
|
|
info!(
|
|
"Scaling efficiency with {} GPUs: {:.1}%",
|
|
target_gpus, efficiency
|
|
);
|
|
Ok(efficiency)
|
|
}
|
|
|
|
/// Handle GPU failure and attempt recovery
|
|
pub async fn handle_gpu_failure(&mut self, failed_gpu_rank: usize) -> Result<Duration> {
|
|
let start_time = Instant::now();
|
|
warn!("Handling failure of GPU rank {}", failed_gpu_rank);
|
|
|
|
// Validate failed GPU rank
|
|
if failed_gpu_rank >= self.world_size {
|
|
return Err(DistributedError::fault_tolerance(format!(
|
|
"Invalid failed GPU rank: {failed_gpu_rank}"
|
|
)));
|
|
}
|
|
|
|
// Mark GPU as failed
|
|
self.fault_tolerance.mark_gpu_failed(failed_gpu_rank)?;
|
|
|
|
// Attempt to checkpoint current state
|
|
self.create_checkpoint().await?;
|
|
|
|
// Redistribute work to remaining healthy GPUs
|
|
self.redistribute_work().await?;
|
|
|
|
// Update process group to exclude failed GPU
|
|
self.update_process_group_for_failure(failed_gpu_rank)
|
|
.await?;
|
|
|
|
let recovery_time = start_time.elapsed();
|
|
|
|
// Update metrics
|
|
{
|
|
let mut metrics = self.metrics.write().await;
|
|
metrics.fault_recovery_time_ms = recovery_time.as_millis() as f64;
|
|
}
|
|
|
|
info!(
|
|
"GPU {} failure recovery completed in {:?}",
|
|
failed_gpu_rank, recovery_time
|
|
);
|
|
Ok(recovery_time)
|
|
}
|
|
|
|
/// Check memory balance across GPUs
|
|
pub async fn check_memory_balance(&mut self) -> Result<f64> {
|
|
debug!("Checking memory balance across {} GPUs", self.world_size);
|
|
|
|
// Update memory usage statistics
|
|
self.load_balancer.update_memory_usage().await?;
|
|
|
|
let memory_usages = &self.load_balancer.memory_usage;
|
|
|
|
if memory_usages.is_empty() {
|
|
return Ok(0.0);
|
|
}
|
|
|
|
let max_usage = *memory_usages.iter().max().unwrap() as f64;
|
|
let min_usage = *memory_usages.iter().min().unwrap() as f64;
|
|
|
|
let imbalance_ratio = if max_usage > 0.0 {
|
|
(max_usage - min_usage) / max_usage
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Update metrics
|
|
{
|
|
let mut metrics = self.metrics.write().await;
|
|
metrics.memory_imbalance_ratio = imbalance_ratio;
|
|
}
|
|
|
|
debug!("Memory imbalance ratio: {:.1}%", imbalance_ratio * 100.0);
|
|
Ok(imbalance_ratio)
|
|
}
|
|
|
|
/// Measure communication overhead
|
|
pub async fn measure_communication_overhead(&mut self) -> Result<f64> {
|
|
debug!("Measuring communication overhead");
|
|
|
|
// Benchmark pure computation time
|
|
let computation_time = self.benchmark_computation_time().await?;
|
|
|
|
// Benchmark computation + communication time
|
|
let total_time = self.benchmark_computation_with_communication().await?;
|
|
|
|
let overhead_percent = if computation_time > 0.0 {
|
|
((total_time - computation_time) / computation_time) * 100.0
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
// Update metrics
|
|
{
|
|
let mut metrics = self.metrics.write().await;
|
|
metrics.communication_overhead_percent = overhead_percent;
|
|
}
|
|
|
|
debug!("Communication overhead: {:.1}%", overhead_percent);
|
|
Ok(overhead_percent)
|
|
}
|
|
|
|
/// Get current training metrics
|
|
pub async fn get_metrics(&self) -> TrainingMetrics {
|
|
self.metrics.read().await.clone()
|
|
}
|
|
|
|
/// Synchronize gradients with dynamic loss scaling
|
|
pub async fn synchronize_gradients_with_loss_scaling(
|
|
&mut self,
|
|
gradients: &mut [Tensor],
|
|
) -> Result<()> {
|
|
let start_time = Instant::now();
|
|
debug!("Starting gradient synchronization with loss scaling");
|
|
|
|
// Scale gradients before synchronization
|
|
{
|
|
let scaler = self.loss_scaler.read().await;
|
|
scaler.scale_gradients(gradients)?;
|
|
}
|
|
|
|
// Perform gradient synchronization
|
|
self.synchronize_gradients(gradients).await?;
|
|
|
|
// Check for overflow and update loss scale
|
|
let has_overflow = {
|
|
let scaler = self.loss_scaler.read().await;
|
|
scaler.has_overflow(gradients)?
|
|
};
|
|
|
|
// Unscale gradients if no overflow
|
|
if !has_overflow {
|
|
let scaler = self.loss_scaler.read().await;
|
|
scaler.unscale_gradients(gradients)?;
|
|
}
|
|
|
|
// Update loss scale based on overflow detection
|
|
{
|
|
let mut scaler = self.loss_scaler.write().await;
|
|
scaler.update_scale(has_overflow);
|
|
}
|
|
|
|
// Zero out gradients if overflow detected
|
|
if has_overflow {
|
|
warn!("Gradient overflow detected, zeroing gradients");
|
|
for gradient in gradients.iter_mut() {
|
|
*gradient = Tensor::zeros(gradient.shape().dims(), gradient.device())?;
|
|
}
|
|
}
|
|
|
|
let sync_time = start_time.elapsed().as_millis() as f64;
|
|
{
|
|
let mut metrics = self.metrics.write().await;
|
|
metrics.gradient_sync_time_ms = sync_time;
|
|
}
|
|
|
|
debug!(
|
|
"Gradient synchronization with loss scaling completed in {:.2}ms",
|
|
sync_time
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get current loss scale
|
|
pub async fn get_loss_scale(&self) -> Result<f32> {
|
|
let scaler = self.loss_scaler.read().await;
|
|
Ok(scaler.scale)
|
|
}
|
|
|
|
/// Set loss scale
|
|
pub async fn set_loss_scale(&mut self, scale: f32) -> Result<()> {
|
|
let mut scaler = self.loss_scaler.write().await;
|
|
scaler.scale = scale.max(scaler.min_scale).min(scaler.max_scale);
|
|
Ok(())
|
|
}
|
|
|
|
/// Set mixed precision mode
|
|
pub async fn set_mixed_precision_mode(&mut self, mode: &str) -> Result<()> {
|
|
let mut scaler = self.loss_scaler.write().await;
|
|
scaler.precision_mode = mode.to_string();
|
|
|
|
// Adjust default scale based on precision mode
|
|
match mode {
|
|
"fp16" => {
|
|
scaler.scale = 65536.0; // 2^16
|
|
scaler.enabled = true;
|
|
}
|
|
"bf16" => {
|
|
scaler.scale = 256.0; // Lower scale for bfloat16
|
|
scaler.enabled = true;
|
|
}
|
|
"fp32" => {
|
|
scaler.scale = 1.0; // No scaling needed for fp32
|
|
scaler.enabled = false;
|
|
}
|
|
_ => {
|
|
return Err(DistributedError::configuration(format!(
|
|
"Unknown precision mode: {mode}"
|
|
)));
|
|
}
|
|
}
|
|
|
|
info!(
|
|
"Mixed precision mode set to {} with scale {}",
|
|
mode, scaler.scale
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
// Private helper methods for benchmarking and recovery
|
|
|
|
async fn benchmark_single_gpu_throughput(&self) -> Result<f64> {
|
|
// Simulate single GPU benchmark
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
Ok(1000.0) // samples/sec
|
|
}
|
|
|
|
async fn benchmark_multi_gpu_throughput(&self, _num_gpus: usize) -> Result<f64> {
|
|
// Simulate multi-GPU benchmark
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
Ok(3500.0) // samples/sec for 4 GPUs (87.5% efficiency)
|
|
}
|
|
|
|
async fn benchmark_computation_time(&self) -> Result<f64> {
|
|
// Simulate computation benchmark
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
Ok(100.0) // milliseconds
|
|
}
|
|
|
|
async fn benchmark_computation_with_communication(&self) -> Result<f64> {
|
|
// Simulate computation + communication benchmark
|
|
tokio::time::sleep(Duration::from_millis(8)).await;
|
|
Ok(104.0) // milliseconds (4% overhead)
|
|
}
|
|
|
|
async fn create_checkpoint(&mut self) -> Result<()> {
|
|
debug!("Creating checkpoint for fault tolerance");
|
|
self.fault_tolerance.last_checkpoint = Instant::now();
|
|
Ok(())
|
|
}
|
|
|
|
async fn redistribute_work(&mut self) -> Result<()> {
|
|
debug!("Redistributing work after GPU failure");
|
|
self.load_balancer
|
|
.rebalance_after_failure(&self.fault_tolerance.failed_gpus)
|
|
.await
|
|
}
|
|
|
|
async fn update_process_group_for_failure(&mut self, failed_rank: usize) -> Result<()> {
|
|
debug!(
|
|
"Updating process group after GPU failure on rank {}",
|
|
failed_rank
|
|
);
|
|
|
|
// Mark the failed rank as unavailable
|
|
self.mark_rank_failed(failed_rank).await?;
|
|
|
|
// Recalculate load distribution among remaining healthy ranks
|
|
self.rebalance_after_failure().await?;
|
|
|
|
// Update communication topology to exclude failed rank
|
|
self.update_communication_topology(failed_rank).await?;
|
|
|
|
info!(
|
|
"Process group reconfigured after failure on rank {}",
|
|
failed_rank
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
async fn mark_rank_failed(&mut self, failed_rank: usize) -> Result<()> {
|
|
info!(
|
|
"Marking rank {} as failed and removing from active pool",
|
|
failed_rank
|
|
);
|
|
// In a real implementation, this would update internal state
|
|
// to exclude the failed rank from future communications
|
|
Ok(())
|
|
}
|
|
|
|
async fn rebalance_after_failure(&mut self) -> Result<()> {
|
|
debug!("Rebalancing workload after GPU failure");
|
|
// Redistribute work among remaining healthy GPUs
|
|
// This would involve recalculating batch splits, gradient synchronization patterns, etc.
|
|
Ok(())
|
|
}
|
|
|
|
async fn update_communication_topology(&mut self, failed_rank: usize) -> Result<()> {
|
|
debug!(
|
|
"Updating communication topology to exclude rank {}",
|
|
failed_rank
|
|
);
|
|
// Update AllReduce trees, ring topologies, and other communication patterns
|
|
// to route around the failed GPU
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl LoadBalancer {
|
|
/// Create a new load balancer
|
|
fn new(num_gpus: usize) -> Self {
|
|
Self {
|
|
gpu_utilizations: vec![0.0; num_gpus],
|
|
memory_usage: vec![0; num_gpus],
|
|
imbalance_threshold: 0.1, // 10% max imbalance
|
|
distribution_weights: vec![1.0; num_gpus], // Equal weights initially
|
|
}
|
|
}
|
|
|
|
/// Update memory usage statistics for all GPUs
|
|
async fn update_memory_usage(&mut self) -> Result<()> {
|
|
// Simulate memory usage monitoring
|
|
for (i, usage) in self.memory_usage.iter_mut().enumerate() {
|
|
*usage = 1_000_000_000 + (i * 100_000_000) as u64; // 1GB + variance
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Rebalance work distribution after GPU failures
|
|
async fn rebalance_after_failure(&mut self, failed_gpus: &[usize]) -> Result<()> {
|
|
debug!("Rebalancing work after GPU failures: {:?}", failed_gpus);
|
|
|
|
let healthy_gpu_count = self.distribution_weights.len() - failed_gpus.len();
|
|
if healthy_gpu_count == 0 {
|
|
return Err(DistributedError::fault_tolerance(
|
|
"No healthy GPUs remaining",
|
|
));
|
|
}
|
|
|
|
// Reset weights for failed GPUs and redistribute to healthy GPUs
|
|
let total_work = self.distribution_weights.iter().sum::<f64>();
|
|
let work_per_healthy_gpu = total_work / healthy_gpu_count as f64;
|
|
|
|
for (i, weight) in self.distribution_weights.iter_mut().enumerate() {
|
|
if failed_gpus.contains(&i) {
|
|
*weight = 0.0;
|
|
} else {
|
|
*weight = work_per_healthy_gpu;
|
|
}
|
|
}
|
|
|
|
debug!(
|
|
"New work distribution weights: {:?}",
|
|
self.distribution_weights
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl FaultTolerance {
|
|
/// Create a new fault tolerance manager
|
|
fn new(num_gpus: usize) -> Self {
|
|
Self {
|
|
health_monitors: vec![true; num_gpus],
|
|
recovery_timeout: Duration::from_secs(30),
|
|
failed_gpus: Vec::new(),
|
|
checkpoint_interval: Duration::from_secs(300), // 5 minutes
|
|
last_checkpoint: Instant::now(),
|
|
}
|
|
}
|
|
|
|
/// Check health status of all GPUs
|
|
async fn check_gpu_health(&mut self) -> Result<()> {
|
|
// Simulate GPU health monitoring
|
|
for (i, healthy) in self.health_monitors.iter().enumerate() {
|
|
if !healthy && !self.failed_gpus.contains(&i) {
|
|
warn!("Detected failed GPU: {}", i);
|
|
self.failed_gpus.push(i);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Mark a GPU as failed
|
|
|
|
fn mark_gpu_failed(&mut self, gpu_rank: usize) -> Result<()> {
|
|
if gpu_rank >= self.health_monitors.len() {
|
|
return Err(DistributedError::fault_tolerance(format!(
|
|
"Invalid GPU rank: {gpu_rank}"
|
|
)));
|
|
}
|
|
|
|
self.health_monitors[gpu_rank] = false;
|
|
if !self.failed_gpus.contains(&gpu_rank) {
|
|
self.failed_gpus.push(gpu_rank);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl ScalingOptimizer {
|
|
/// Create a new scaling optimizer
|
|
fn new() -> Self {
|
|
Self {
|
|
communication_overlap_enabled: true,
|
|
communication_streams: 4,
|
|
compute_streams: 8,
|
|
ring_buffer_size: 1024 * 1024, // 1MB
|
|
gradient_compression: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DynamicLossScaler {
|
|
/// Create a new dynamic loss scaler
|
|
fn new() -> Self {
|
|
Self {
|
|
scale: 65536.0, // 2^16, typical starting scale for fp16
|
|
growth_factor: 2.0,
|
|
backoff_factor: 0.5,
|
|
steps_without_overflow: 0,
|
|
growth_interval: 2000, // Grow scale every 2000 steps without overflow
|
|
min_scale: 1.0,
|
|
max_scale: 65536.0 * 65536.0, // 2^32
|
|
precision_mode: "fp32".to_string(),
|
|
enabled: true,
|
|
}
|
|
}
|
|
|
|
/// Check if gradients have overflow (inf or nan values)
|
|
fn has_overflow(&self, gradients: &[Tensor]) -> Result<bool> {
|
|
for gradient in gradients {
|
|
let data = gradient.data()?;
|
|
for &value in &data {
|
|
if !value.is_finite() {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
Ok(false)
|
|
}
|
|
|
|
/// Update the loss scale based on overflow detection
|
|
fn update_scale(&mut self, has_overflow: bool) {
|
|
if has_overflow {
|
|
// Reduce scale on overflow
|
|
self.scale *= self.backoff_factor;
|
|
self.scale = self.scale.max(self.min_scale);
|
|
self.steps_without_overflow = 0;
|
|
debug!("Loss scale reduced to {} due to overflow", self.scale);
|
|
} else {
|
|
// Increase steps without overflow
|
|
self.steps_without_overflow += 1;
|
|
|
|
// Grow scale if no overflow for growth_interval steps
|
|
if self.steps_without_overflow >= self.growth_interval {
|
|
self.scale *= self.growth_factor;
|
|
self.scale = self.scale.min(self.max_scale);
|
|
self.steps_without_overflow = 0;
|
|
debug!(
|
|
"Loss scale increased to {} after {} stable steps",
|
|
self.scale, self.growth_interval
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Scale gradients for mixed precision training
|
|
fn scale_gradients(&self, gradients: &mut [Tensor]) -> Result<()> {
|
|
if !self.enabled || self.scale == 1.0 {
|
|
return Ok(());
|
|
}
|
|
|
|
for gradient in gradients.iter_mut() {
|
|
*gradient = gradient.mul_scalar(self.scale)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Unscale gradients after all-reduce
|
|
fn unscale_gradients(&self, gradients: &mut [Tensor]) -> Result<()> {
|
|
if !self.enabled || self.scale == 1.0 {
|
|
return Ok(());
|
|
}
|
|
|
|
let inv_scale = 1.0 / self.scale;
|
|
for gradient in gradients.iter_mut() {
|
|
*gradient = gradient.mul_scalar(inv_scale)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|