Files
rustytorch/crates/training/rtx-distributed/src/async_grad.rs
T
2026-03-04 00:08:42 +00:00

838 lines
25 KiB
Rust

//! 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::<f32>();
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<Instant>,
/// End time
pub end_time: Option<Instant>,
/// Error message if failed
pub error: Option<String>,
/// 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<Duration> {
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<GradientEntry>,
/// 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<GradientEntry> {
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<GradientCompressor>,
/// Buckets
buckets: Vec<GradientBucket>,
/// Current bucket index for new gradients
current_bucket: usize,
/// Pending AllReduce handles
pending_handles: VecDeque<AsyncAllReduceHandle>,
/// Completed AllReduce handles
completed_handles: Vec<AsyncAllReduceHandle>,
/// Next operation ID
next_op_id: usize,
/// Gradient name to bucket mapping
gradient_bucket_map: HashMap<String, usize>,
/// 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<Option<usize>> {
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<usize> {
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<Vec<usize>> {
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<RwLock<AsyncGradAggregator>>;
/// 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<PipelineMessage>,
/// Aggregator (shared with background task)
aggregator: SharedAsyncGradAggregator,
/// Whether pipeline is running
running: Arc<std::sync::atomic::AtomicBool>,
}
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);
}
}