torsh-optim 0.1.3

Optimization algorithms for ToRSh with PyTorch-compatible API
Documentation
//! Core distributed optimizer functionality
//!
//! This module provides the main distributed optimizer wrapper and core types
//! for synchronizing gradients across multiple processes in distributed training.

use crate::{Optimizer, OptimizerResult, OptimizerState};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use torsh_core::error::{Result, TorshError};
use torsh_tensor::Tensor;

/// Communication backend for distributed training
#[derive(Debug, Clone)]
pub enum DistributedBackend {
    /// NCCL backend for NVIDIA GPUs
    NCCL,
    /// MPI backend for general distributed computing
    MPI,
    /// Gloo backend for CPU and GPU
    Gloo,
    /// Custom backend
    Custom(String),
}

/// Gradient synchronization strategy
#[derive(Debug, Clone)]
pub enum SyncStrategy {
    /// AllReduce: Sum gradients across all processes
    AllReduce,
    /// AllGather: Gather all gradients and average
    AllGather,
    /// ReduceScatter: Distribute gradient reduction across processes
    ReduceScatter,
}

/// Configuration for distributed optimizer
#[derive(Debug, Clone)]
pub struct DistributedConfig {
    /// Communication backend
    pub backend: DistributedBackend,
    /// Gradient synchronization strategy
    pub sync_strategy: SyncStrategy,
    /// World size (total number of processes)
    pub world_size: usize,
    /// Current process rank
    pub rank: usize,
    /// Process group for communication
    pub process_group: Option<String>,
    /// Whether to enable gradient compression
    pub gradient_compression: bool,
    /// Bucket size for gradient bucketing (in MB)
    pub bucket_size_mb: f32,
    /// Whether to overlap communication with computation
    pub overlap_communication: bool,
}

impl Default for DistributedConfig {
    fn default() -> Self {
        Self {
            backend: DistributedBackend::Gloo,
            sync_strategy: SyncStrategy::AllReduce,
            world_size: 1,
            rank: 0,
            process_group: None,
            gradient_compression: false,
            bucket_size_mb: 25.0,
            overlap_communication: true,
        }
    }
}

/// Distributed optimizer wrapper
///
/// This wrapper can be applied to any optimizer to enable distributed training.
/// It handles gradient synchronization across multiple processes before applying
/// the underlying optimizer's update rule.
pub struct DistributedOptimizer<O: Optimizer> {
    optimizer: O,
    config: DistributedConfig,
    gradient_buckets: Vec<GradientBucket>,
    #[allow(dead_code)]
    communication_handle: Option<CommunicationHandle>,
}

/// Gradient bucket for efficient communication
#[derive(Debug)]
pub struct GradientBucket {
    pub tensors: Vec<Arc<RwLock<Tensor>>>,
    #[allow(dead_code)]
    pub flattened_grad: Option<Tensor>,
    pub size_bytes: usize,
}

/// Handle for asynchronous communication
#[derive(Debug)]
pub struct CommunicationHandle {
    #[allow(dead_code)]
    pub operation_id: u64,
}

/// Communication statistics for monitoring
#[derive(Debug, Default, Clone)]
pub struct CommunicationStats {
    pub total_communications: u64,
    pub total_bytes_transferred: u64,
    pub average_communication_time_ms: f32,
    pub gradient_compression_ratio: f32,
}

impl<O: Optimizer> DistributedOptimizer<O> {
    /// Create a new distributed optimizer
    pub fn new(optimizer: O, config: DistributedConfig) -> OptimizerResult<Self> {
        let gradient_buckets = Vec::new();

        Ok(Self {
            optimizer,
            config,
            gradient_buckets,
            communication_handle: None,
        })
    }

    /// Get the underlying optimizer
    pub fn inner(&self) -> &O {
        &self.optimizer
    }

    /// Get the underlying optimizer mutably
    pub fn inner_mut(&mut self) -> &mut O {
        &mut self.optimizer
    }

    /// Get the configuration
    pub fn config(&self) -> &DistributedConfig {
        &self.config
    }

    /// Update the configuration
    pub fn set_config(&mut self, config: DistributedConfig) {
        self.config = config;
    }

    /// Get communication statistics
    pub fn get_communication_stats(&self) -> CommunicationStats {
        // In a real implementation, this would return actual statistics
        CommunicationStats::default()
    }

    /// Synchronize gradients across all processes
    pub fn synchronize_gradients(&mut self) -> OptimizerResult<()> {
        // In a real implementation, this would perform actual gradient synchronization
        // For now, this is a placeholder that would integrate with communication backends

        match self.config.sync_strategy {
            SyncStrategy::AllReduce => self.all_reduce_gradients(),
            SyncStrategy::AllGather => self.all_gather_gradients(),
            SyncStrategy::ReduceScatter => self.reduce_scatter_gradients(),
        }
    }

    /// Perform all-reduce on gradients
    fn all_reduce_gradients(&mut self) -> OptimizerResult<()> {
        // Placeholder for all-reduce implementation
        // In reality, this would call into the communication backend
        Ok(())
    }

    /// Perform all-gather on gradients
    fn all_gather_gradients(&mut self) -> OptimizerResult<()> {
        // Placeholder for all-gather implementation
        Ok(())
    }

    /// Perform reduce-scatter on gradients
    fn reduce_scatter_gradients(&mut self) -> OptimizerResult<()> {
        // Placeholder for reduce-scatter implementation
        Ok(())
    }

    /// Create gradient buckets for efficient communication
    pub fn create_gradient_buckets(
        &mut self,
        parameters: &[Arc<RwLock<Tensor>>],
    ) -> OptimizerResult<()> {
        let bucket_size_bytes = (self.config.bucket_size_mb * 1024.0 * 1024.0) as usize;
        let mut current_bucket = GradientBucket {
            tensors: Vec::new(),
            flattened_grad: None,
            size_bytes: 0,
        };

        for param in parameters {
            let param_guard = param.read();
            let param_size = param_guard.shape().numel() * 4; // Assuming f32

            if current_bucket.size_bytes + param_size > bucket_size_bytes
                && !current_bucket.tensors.is_empty()
            {
                // Start a new bucket
                self.gradient_buckets.push(current_bucket);
                current_bucket = GradientBucket {
                    tensors: Vec::new(),
                    flattened_grad: None,
                    size_bytes: 0,
                };
            }

            current_bucket.tensors.push(param.clone());
            current_bucket.size_bytes += param_size;
        }

        // Add the last bucket if it has any tensors
        if !current_bucket.tensors.is_empty() {
            self.gradient_buckets.push(current_bucket);
        }

        Ok(())
    }

    /// Flatten gradients within a bucket for efficient communication
    fn flatten_bucket_gradients(&self, bucket: &GradientBucket) -> OptimizerResult<Tensor> {
        // This would flatten all gradients in the bucket into a single tensor
        // For now, return a placeholder
        let total_elements: usize = bucket
            .tensors
            .iter()
            .map(|t| {
                let guard = t.read();
                guard.shape().numel()
            })
            .sum();

        // Return a zero tensor as placeholder
        let flattened = Tensor::zeros(&[total_elements], torsh_core::device::DeviceType::Cpu)?;
        Ok(flattened)
    }

    /// Unflatten gradients after communication
    fn unflatten_bucket_gradients(
        &self,
        bucket: &GradientBucket,
        flattened: &Tensor,
    ) -> OptimizerResult<()> {
        // This would unflatten the communicated gradients back to individual tensors
        // For now, this is a placeholder
        Ok(())
    }
}

impl<O: Optimizer> Optimizer for DistributedOptimizer<O> {
    fn step(&mut self) -> OptimizerResult<()> {
        // Synchronize gradients before optimization step
        self.synchronize_gradients()?;

        // Perform the optimization step
        self.optimizer.step()
    }

    fn zero_grad(&mut self) {
        self.optimizer.zero_grad();
    }

    fn get_lr(&self) -> Vec<f32> {
        self.optimizer.get_lr()
    }

    fn set_lr(&mut self, lr: f32) {
        self.optimizer.set_lr(lr);
    }

    fn add_param_group(&mut self, params: Vec<Arc<RwLock<Tensor>>>, options: HashMap<String, f32>) {
        self.optimizer.add_param_group(params, options);
    }

    fn parameters(&self) -> Vec<Arc<RwLock<Tensor>>> {
        self.optimizer.parameters()
    }

    fn state_dict(&self) -> OptimizerResult<OptimizerState> {
        self.optimizer.state_dict()
    }

    fn load_state_dict(&mut self, state: OptimizerState) -> OptimizerResult<()> {
        self.optimizer.load_state_dict(state)
    }
}

/// Extension trait for adding distributed functionality to any optimizer
pub trait OptimizerExt: Optimizer + Sized {
    /// Wrap this optimizer with distributed functionality
    fn distributed(self, config: DistributedConfig) -> OptimizerResult<DistributedOptimizer<Self>> {
        DistributedOptimizer::new(self, config)
    }
}

impl<O: Optimizer> OptimizerExt for O {}