//! Async Gradient Aggregation //! //! This module provides asynchronous gradient aggregation for distributed training: //! - Pipelined AllReduce overlapped with backward pass //! - Gradient bucketing for reduced kernel launch overhead //! - Double-buffering for seamless compute/communication overlap //! - Priority scheduling based on backward pass order //! //! The async gradient pipeline enables near-perfect overlap between //! gradient computation and communication, minimizing idle GPU time. use crate::comm::ReduceOp; use crate::error::{DistributedError, Result}; use crate::gradient_compression::{CompressionConfig, GradientCompressor}; use crate::group::ProcessGroup; use parking_lot::RwLock; use rtx_tensor::Tensor; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::mpsc; // ============================================================================= // Configuration // ============================================================================= /// Configuration for async gradient aggregation #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AsyncGradConfig { /// Enable async gradient aggregation pub enabled: bool, /// Number of gradient buckets pub num_buckets: usize, /// Bucket size in bytes pub bucket_size_bytes: usize, /// Maximum concurrent AllReduce operations pub max_concurrent_allreduce: usize, /// Enable gradient compression pub compression: bool, /// Compression ratio target (if compression enabled) pub compression_ratio: f32, /// Use separate stream for AllReduce pub use_comm_stream: bool, /// Timeout for AllReduce operations (ms) pub allreduce_timeout_ms: u64, /// Enable error recovery pub error_recovery: bool, } impl Default for AsyncGradConfig { fn default() -> Self { Self { enabled: true, num_buckets: 4, bucket_size_bytes: 25 * 1024 * 1024, // 25MB max_concurrent_allreduce: 2, compression: false, compression_ratio: 0.1, use_comm_stream: true, allreduce_timeout_ms: 30000, error_recovery: true, } } } // ============================================================================= // Gradient Entry // ============================================================================= /// A gradient ready for aggregation #[derive(Debug)] pub struct GradientEntry { /// Parameter name pub name: String, /// Gradient tensor pub gradient: Tensor, /// Backward pass order (lower = computed earlier) pub backward_order: usize, /// Timestamp when gradient was computed pub computed_at: Instant, /// Size in bytes pub size_bytes: usize, } impl GradientEntry { /// Create a new gradient entry pub fn new(name: String, gradient: Tensor, backward_order: usize) -> Self { let size_bytes = gradient.numel() * std::mem::size_of::(); Self { name, gradient, backward_order, computed_at: Instant::now(), size_bytes, } } /// Get age of this gradient pub fn age(&self) -> Duration { self.computed_at.elapsed() } } // ============================================================================= // Async AllReduce Handle // ============================================================================= /// State of an async AllReduce operation #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AllReduceState { /// Queued, waiting to start Queued, /// Currently executing InProgress, /// Completed successfully Completed, /// Failed with error Failed, /// Cancelled Cancelled, } /// Handle to track an async AllReduce operation #[derive(Debug)] pub struct AsyncAllReduceHandle { /// Unique operation ID pub id: usize, /// Bucket ID being reduced pub bucket_id: usize, /// Current state pub state: AllReduceState, /// Start time pub start_time: Option, /// End time pub end_time: Option, /// Error message if failed pub error: Option, /// Number of gradients in this operation pub gradient_count: usize, /// Total bytes pub total_bytes: usize, } impl AsyncAllReduceHandle { /// Create a new handle pub fn new(id: usize, bucket_id: usize, gradient_count: usize, total_bytes: usize) -> Self { Self { id, bucket_id, state: AllReduceState::Queued, start_time: None, end_time: None, error: None, gradient_count, total_bytes, } } /// Mark as started pub fn start(&mut self) { self.state = AllReduceState::InProgress; self.start_time = Some(Instant::now()); } /// Mark as completed pub fn complete(&mut self) { self.state = AllReduceState::Completed; self.end_time = Some(Instant::now()); } /// Mark as failed pub fn fail(&mut self, error: String) { self.state = AllReduceState::Failed; self.end_time = Some(Instant::now()); self.error = Some(error); } /// Get duration if completed pub fn duration(&self) -> Option { match (self.start_time, self.end_time) { (Some(start), Some(end)) => Some(end.duration_since(start)), _ => None, } } /// Check if operation is done (completed or failed) pub fn is_done(&self) -> bool { matches!( self.state, AllReduceState::Completed | AllReduceState::Failed | AllReduceState::Cancelled ) } } // ============================================================================= // Gradient Bucket // ============================================================================= /// A bucket of gradients for batched AllReduce #[derive(Debug)] pub struct GradientBucket { /// Bucket ID pub id: usize, /// Gradients in this bucket gradients: Vec, /// Total size in bytes pub size_bytes: usize, /// Maximum size max_size_bytes: usize, /// Whether bucket is ready for AllReduce pub is_ready: bool, /// Whether AllReduce has started pub allreduce_started: bool, /// Whether AllReduce is complete pub allreduce_complete: bool, } impl GradientBucket { /// Create a new bucket pub fn new(id: usize, max_size_bytes: usize) -> Self { Self { id, gradients: Vec::new(), size_bytes: 0, max_size_bytes, is_ready: false, allreduce_started: false, allreduce_complete: false, } } /// Add a gradient to the bucket pub fn add(&mut self, entry: GradientEntry) -> bool { if self.is_ready || self.size_bytes + entry.size_bytes > self.max_size_bytes { return false; } self.size_bytes += entry.size_bytes; self.gradients.push(entry); true } /// Check if bucket is full pub fn is_full(&self) -> bool { self.size_bytes >= self.max_size_bytes } /// Mark bucket as ready for AllReduce pub fn mark_ready(&mut self) { self.is_ready = true; } /// Get gradients pub fn gradients(&self) -> &[GradientEntry] { &self.gradients } /// Take ownership of gradients pub fn take_gradients(&mut self) -> Vec { std::mem::take(&mut self.gradients) } /// Get gradient count pub fn len(&self) -> usize { self.gradients.len() } /// Check if empty pub fn is_empty(&self) -> bool { self.gradients.is_empty() } /// Reset the bucket pub fn reset(&mut self) { self.gradients.clear(); self.size_bytes = 0; self.is_ready = false; self.allreduce_started = false; self.allreduce_complete = false; } } // ============================================================================= // Async Gradient Aggregator // ============================================================================= /// Manages async gradient aggregation pipeline pub struct AsyncGradAggregator { /// Configuration config: AsyncGradConfig, /// Process group for communication process_group: ProcessGroup, /// Gradient compressor (optional) compressor: Option, /// Buckets buckets: Vec, /// Current bucket index for new gradients current_bucket: usize, /// Pending AllReduce handles pending_handles: VecDeque, /// Completed AllReduce handles completed_handles: Vec, /// Next operation ID next_op_id: usize, /// Gradient name to bucket mapping gradient_bucket_map: HashMap, /// Backward order counter backward_order: usize, /// Statistics stats: AggregatorStats, } /// Statistics for the aggregator #[derive(Debug, Default, Clone)] pub struct AggregatorStats { /// Total gradients aggregated pub gradients_aggregated: usize, /// Total bytes aggregated pub bytes_aggregated: usize, /// Total AllReduce operations pub allreduce_ops: usize, /// Successful operations pub successful_ops: usize, /// Failed operations pub failed_ops: usize, /// Total aggregation time (ms) pub total_time_ms: f64, /// Average latency per gradient (ms) pub avg_latency_ms: f64, /// Throughput (GB/s) pub throughput_gbps: f64, /// Overlap efficiency (0-1) pub overlap_efficiency: f32, } impl AsyncGradAggregator { /// Create a new async gradient aggregator pub fn new(config: AsyncGradConfig, process_group: ProcessGroup) -> Self { let num_buckets = config.num_buckets.max(1); let bucket_size = config.bucket_size_bytes; let buckets: Vec<_> = (0..num_buckets) .map(|i| GradientBucket::new(i, bucket_size)) .collect(); let compressor = if config.compression { Some(GradientCompressor::new(CompressionConfig::default())) } else { None }; Self { config, process_group, compressor, buckets, current_bucket: 0, pending_handles: VecDeque::new(), completed_handles: Vec::new(), next_op_id: 0, gradient_bucket_map: HashMap::new(), backward_order: 0, stats: AggregatorStats::default(), } } /// Register a gradient (called during backward pass) pub fn register_gradient(&mut self, name: &str, gradient: Tensor) -> Result> { let entry = GradientEntry::new(name.to_string(), gradient, self.backward_order); self.backward_order += 1; // Find or assign bucket let bucket_id = if let Some(&id) = self.gradient_bucket_map.get(name) { id } else { let id = self.current_bucket; self.gradient_bucket_map.insert(name.to_string(), id); id }; // Add to bucket let bucket = &mut self.buckets[bucket_id]; if !bucket.add(entry) { // Bucket is full, try next bucket self.current_bucket = (self.current_bucket + 1) % self.buckets.len(); let next_bucket = &mut self.buckets[self.current_bucket]; self.gradient_bucket_map .insert(name.to_string(), self.current_bucket); let entry = GradientEntry::new( name.to_string(), Tensor::zeros(rtx_tensor::Shape::new(vec![1])?, &rtx_tensor::Device::Cpu)?, self.backward_order - 1, ); next_bucket.add(entry); } // Check if bucket is ready let bucket = &mut self.buckets[bucket_id]; if bucket.is_full() && !bucket.is_ready { bucket.mark_ready(); return Ok(Some(bucket_id)); } Ok(None) } /// Start AllReduce for a bucket pub async fn start_allreduce(&mut self, bucket_id: usize) -> Result { let bucket = &mut self.buckets[bucket_id]; if bucket.allreduce_started { return Err(DistributedError::configuration(format!( "AllReduce already started for bucket {}", bucket_id ))); } bucket.allreduce_started = true; // Create handle let op_id = self.next_op_id; self.next_op_id += 1; let mut handle = AsyncAllReduceHandle::new(op_id, bucket_id, bucket.len(), bucket.size_bytes); handle.start(); // Perform AllReduce on each gradient in the bucket let start = Instant::now(); let mut success = true; let mut error_msg = None; for entry in &mut bucket.gradients { let result = self .process_group .all_reduce(&mut entry.gradient, ReduceOp::Sum) .await; if let Err(e) = result { success = false; error_msg = Some(e.to_string()); break; } // Average the gradient let world_size = self.process_group.world_size() as f32; if let Err(e) = entry.gradient.div_scalar(world_size) { success = false; error_msg = Some(e.to_string()); break; } } let duration = start.elapsed(); // Update handle if success { handle.complete(); self.stats.successful_ops += 1; } else { handle.fail(error_msg.unwrap_or_else(|| "Unknown error".to_string())); self.stats.failed_ops += 1; } // Update bucket bucket.allreduce_complete = true; // Update stats self.stats.allreduce_ops += 1; self.stats.gradients_aggregated += bucket.len(); self.stats.bytes_aggregated += bucket.size_bytes; self.stats.total_time_ms += duration.as_secs_f64() * 1000.0; // Move handle to completed self.completed_handles.push(handle); Ok(op_id) } /// Process all ready buckets pub async fn process_ready_buckets(&mut self) -> Result> { let mut completed = Vec::new(); for bucket_id in 0..self.buckets.len() { let bucket = &self.buckets[bucket_id]; if bucket.is_ready && !bucket.allreduce_started { // Check concurrent limit let in_progress = self .pending_handles .iter() .filter(|h| h.state == AllReduceState::InProgress) .count(); if in_progress < self.config.max_concurrent_allreduce { let op_id = self.start_allreduce(bucket_id).await?; completed.push(op_id); } } } Ok(completed) } /// Flush all remaining gradients (end of backward pass) pub async fn flush(&mut self) -> Result<()> { // Mark all non-empty buckets as ready for bucket in &mut self.buckets { if !bucket.is_empty() && !bucket.is_ready { bucket.mark_ready(); } } // Process all ready buckets for bucket_id in 0..self.buckets.len() { let bucket = &self.buckets[bucket_id]; if bucket.is_ready && !bucket.allreduce_started { self.start_allreduce(bucket_id).await?; } } Ok(()) } /// Wait for all pending operations to complete pub async fn wait_all(&self) -> Result<()> { // In real implementation, would wait on async handles Ok(()) } /// Get aggregated gradient for a parameter pub fn get_gradient(&self, name: &str) -> Option<&Tensor> { let bucket_id = self.gradient_bucket_map.get(name)?; let bucket = &self.buckets[*bucket_id]; bucket .gradients() .iter() .find(|e| e.name == name) .map(|e| &e.gradient) } /// Reset for next iteration pub fn reset(&mut self) { for bucket in &mut self.buckets { bucket.reset(); } self.current_bucket = 0; self.pending_handles.clear(); self.completed_handles.clear(); self.backward_order = 0; } /// Get statistics pub fn stats(&self) -> &AggregatorStats { &self.stats } /// Calculate throughput pub fn calculate_throughput(&mut self) { if self.stats.total_time_ms > 0.0 { let bytes = self.stats.bytes_aggregated as f64; let seconds = self.stats.total_time_ms / 1000.0; self.stats.throughput_gbps = (bytes / (1024.0 * 1024.0 * 1024.0)) / seconds; } if self.stats.gradients_aggregated > 0 { self.stats.avg_latency_ms = self.stats.total_time_ms / self.stats.gradients_aggregated as f64; } } } // ============================================================================= // Thread-Safe Wrapper // ============================================================================= /// Thread-safe wrapper for AsyncGradAggregator pub type SharedAsyncGradAggregator = Arc>; /// Create a shared async gradient aggregator pub fn shared_async_grad_aggregator( config: AsyncGradConfig, process_group: ProcessGroup, ) -> SharedAsyncGradAggregator { Arc::new(RwLock::new(AsyncGradAggregator::new(config, process_group))) } // ============================================================================= // Gradient Pipeline // ============================================================================= /// Message types for the gradient pipeline #[derive(Debug)] pub enum PipelineMessage { /// New gradient computed GradientReady { name: String, gradient: Tensor }, /// Bucket ready for AllReduce BucketReady { bucket_id: usize }, /// AllReduce completed AllReduceComplete { op_id: usize, success: bool }, /// Flush all pending gradients Flush, /// Shutdown the pipeline Shutdown, } /// Gradient aggregation pipeline with separate compute and comm threads pub struct GradientPipeline { /// Sender for pipeline messages tx: mpsc::UnboundedSender, /// Aggregator (shared with background task) aggregator: SharedAsyncGradAggregator, /// Whether pipeline is running running: Arc, } impl GradientPipeline { /// Create a new gradient pipeline pub fn new(config: AsyncGradConfig, process_group: ProcessGroup) -> Self { let (tx, _rx) = mpsc::unbounded_channel(); let aggregator = shared_async_grad_aggregator(config, process_group); let running = Arc::new(std::sync::atomic::AtomicBool::new(true)); Self { tx, aggregator, running, } } /// Submit a gradient for aggregation pub fn submit_gradient(&self, name: String, gradient: Tensor) -> Result<()> { self.tx .send(PipelineMessage::GradientReady { name, gradient }) .map_err(|_| DistributedError::communication("pipeline", "Channel closed"))?; Ok(()) } /// Flush all pending gradients pub fn flush(&self) -> Result<()> { self.tx .send(PipelineMessage::Flush) .map_err(|_| DistributedError::communication("pipeline", "Channel closed"))?; Ok(()) } /// Shutdown the pipeline pub fn shutdown(&self) -> Result<()> { self.running .store(false, std::sync::atomic::Ordering::SeqCst); let _ = self.tx.send(PipelineMessage::Shutdown); Ok(()) } /// Get the aggregator pub fn aggregator(&self) -> &SharedAsyncGradAggregator { &self.aggregator } /// Check if pipeline is running pub fn is_running(&self) -> bool { self.running.load(std::sync::atomic::Ordering::SeqCst) } } // ============================================================================= // Tests // ============================================================================= #[cfg(test)] mod tests { use super::*; use crate::backend::{Backend, BackendConfig}; use rtx_tensor::Shape; #[test] fn test_async_grad_config_default() { let config = AsyncGradConfig::default(); assert!(config.enabled); assert_eq!(config.num_buckets, 4); assert_eq!(config.max_concurrent_allreduce, 2); } #[test] fn test_gradient_entry() { let gradient = Tensor::zeros(Shape::new(vec![100]).unwrap(), &rtx_tensor::Device::Cpu).unwrap(); let entry = GradientEntry::new("test".to_string(), gradient, 0); assert_eq!(entry.name, "test"); assert_eq!(entry.backward_order, 0); assert_eq!(entry.size_bytes, 100 * 4); } #[test] fn test_async_allreduce_handle() { let mut handle = AsyncAllReduceHandle::new(0, 0, 10, 1000); assert_eq!(handle.state, AllReduceState::Queued); assert!(!handle.is_done()); handle.start(); assert_eq!(handle.state, AllReduceState::InProgress); handle.complete(); assert_eq!(handle.state, AllReduceState::Completed); assert!(handle.is_done()); assert!(handle.duration().is_some()); } #[test] fn test_gradient_bucket() { let mut bucket = GradientBucket::new(0, 1000); assert!(bucket.is_empty()); assert!(!bucket.is_full()); let gradient = Tensor::zeros(Shape::new(vec![100]).unwrap(), &rtx_tensor::Device::Cpu).unwrap(); let entry = GradientEntry::new("grad1".to_string(), gradient, 0); assert!(bucket.add(entry)); assert_eq!(bucket.len(), 1); assert_eq!(bucket.size_bytes, 400); } #[test] fn test_bucket_full() { let mut bucket = GradientBucket::new(0, 800); // 800 bytes max // Add gradient that fills bucket (200 elements * 4 bytes = 800 bytes) let gradient = Tensor::zeros(Shape::new(vec![200]).unwrap(), &rtx_tensor::Device::Cpu).unwrap(); let entry = GradientEntry::new("grad1".to_string(), gradient, 0); let added = bucket.add(entry); assert!(added, "Gradient should be added"); assert!( bucket.is_full(), "Bucket should be full after adding 800 bytes" ); } #[tokio::test] async fn test_async_grad_aggregator_creation() { let config = AsyncGradConfig::default(); let backend_config = BackendConfig::cpu(); let pg = ProcessGroup::new_with_config(Backend::Cpu, 1, 0, backend_config) .await .unwrap(); let aggregator = AsyncGradAggregator::new(config, pg); assert_eq!(aggregator.stats.gradients_aggregated, 0); assert_eq!(aggregator.buckets.len(), 4); } #[tokio::test] async fn test_register_gradient() { let config = AsyncGradConfig::default(); let backend_config = BackendConfig::cpu(); let pg = ProcessGroup::new_with_config(Backend::Cpu, 1, 0, backend_config) .await .unwrap(); let mut aggregator = AsyncGradAggregator::new(config, pg); let gradient = Tensor::zeros(Shape::new(vec![100]).unwrap(), &rtx_tensor::Device::Cpu).unwrap(); let result = aggregator.register_gradient("layer1.weight", gradient); assert!(result.is_ok()); } #[tokio::test] async fn test_aggregator_reset() { let config = AsyncGradConfig::default(); let backend_config = BackendConfig::cpu(); let pg = ProcessGroup::new_with_config(Backend::Cpu, 1, 0, backend_config) .await .unwrap(); let mut aggregator = AsyncGradAggregator::new(config, pg); let gradient = Tensor::zeros(Shape::new(vec![100]).unwrap(), &rtx_tensor::Device::Cpu).unwrap(); let _ = aggregator.register_gradient("test", gradient); aggregator.reset(); assert!(aggregator.buckets.iter().all(|b| b.is_empty())); assert_eq!(aggregator.backward_order, 0); } #[test] fn test_aggregator_stats_default() { let stats = AggregatorStats::default(); assert_eq!(stats.gradients_aggregated, 0); assert_eq!(stats.allreduce_ops, 0); assert_eq!(stats.throughput_gbps, 0.0); } #[tokio::test] async fn test_gradient_pipeline_creation() { let config = AsyncGradConfig::default(); let backend_config = BackendConfig::cpu(); let pg = ProcessGroup::new_with_config(Backend::Cpu, 1, 0, backend_config) .await .unwrap(); let pipeline = GradientPipeline::new(config, pg); assert!(pipeline.is_running()); } #[test] fn test_allreduce_handle_fail() { let mut handle = AsyncAllReduceHandle::new(0, 0, 10, 1000); handle.start(); handle.fail("Test error".to_string()); assert_eq!(handle.state, AllReduceState::Failed); assert!(handle.is_done()); assert_eq!(handle.error, Some("Test error".to_string())); } #[test] fn test_bucket_mark_ready() { let mut bucket = GradientBucket::new(0, 1000); assert!(!bucket.is_ready); bucket.mark_ready(); assert!(bucket.is_ready); } }