615 lines
15 KiB
Rust
615 lines
15 KiB
Rust
//! Multi-node distributed training support
|
|
|
|
use crate::error::{DistributedError, Result};
|
|
use std::collections::HashMap;
|
|
use std::net::IpAddr;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{Mutex, RwLock};
|
|
use tokio::time::Duration;
|
|
|
|
/// Node role in the cluster
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum NodeRole {
|
|
Master,
|
|
Worker,
|
|
Parameter,
|
|
}
|
|
|
|
/// Node configuration
|
|
#[derive(Debug, Clone)]
|
|
pub struct NodeConfig {
|
|
pub role: NodeRole,
|
|
pub hostname: String,
|
|
pub port: u16,
|
|
pub num_gpus: usize,
|
|
pub fault_tolerance: bool,
|
|
}
|
|
|
|
impl Default for NodeConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
role: NodeRole::Worker,
|
|
hostname: "localhost".to_string(),
|
|
port: 29500,
|
|
num_gpus: 1,
|
|
fault_tolerance: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl NodeConfig {
|
|
pub fn with_role(mut self, role: NodeRole) -> Self {
|
|
self.role = role;
|
|
self
|
|
}
|
|
|
|
pub fn with_port(mut self, port: u16) -> Self {
|
|
self.port = port;
|
|
self
|
|
}
|
|
|
|
pub fn with_fault_tolerance(mut self, enabled: bool) -> Self {
|
|
self.fault_tolerance = enabled;
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Node information
|
|
#[derive(Debug, Clone)]
|
|
pub struct NodeInfo {
|
|
pub node_id: usize,
|
|
pub hostname: String,
|
|
pub ip_address: IpAddr,
|
|
pub port: u16,
|
|
pub role: NodeRole,
|
|
pub num_gpus: usize,
|
|
pub region: Option<String>,
|
|
pub latency_ms: Option<u32>,
|
|
}
|
|
|
|
impl NodeInfo {
|
|
pub fn new(node_id: usize, hostname: &str, address: &str) -> Self {
|
|
let parts: Vec<&str> = address.split(':').collect();
|
|
let ip_str = parts[0];
|
|
let port = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(29500);
|
|
|
|
Self {
|
|
node_id,
|
|
hostname: hostname.to_string(),
|
|
ip_address: ip_str
|
|
.parse()
|
|
.unwrap_or_else(|_| "127.0.0.1".parse().unwrap()),
|
|
port,
|
|
role: NodeRole::Worker,
|
|
num_gpus: 1,
|
|
region: None,
|
|
latency_ms: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_region(mut self, region: &str) -> Self {
|
|
self.region = Some(region.to_string());
|
|
self
|
|
}
|
|
|
|
pub fn with_latency_ms(mut self, latency: u32) -> Self {
|
|
self.latency_ms = Some(latency);
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Multi-node cluster manager
|
|
#[derive(Debug)]
|
|
pub struct MultiNodeCluster {
|
|
config: NodeConfig,
|
|
nodes: Arc<RwLock<HashMap<usize, NodeInfo>>>,
|
|
local_rank: usize,
|
|
world_size: Arc<RwLock<usize>>,
|
|
is_initialized: Arc<RwLock<bool>>,
|
|
jobs: Arc<RwLock<HashMap<String, JobInfo>>>,
|
|
}
|
|
|
|
impl MultiNodeCluster {
|
|
pub async fn new(config: NodeConfig) -> Result<Self> {
|
|
let mut nodes = HashMap::new();
|
|
|
|
// Add self as first node
|
|
let self_info = NodeInfo {
|
|
node_id: 0,
|
|
hostname: config.hostname.clone(),
|
|
ip_address: "127.0.0.1".parse().unwrap(),
|
|
port: config.port,
|
|
role: config.role,
|
|
num_gpus: config.num_gpus,
|
|
region: None,
|
|
latency_ms: None,
|
|
};
|
|
|
|
nodes.insert(0, self_info);
|
|
|
|
Ok(Self {
|
|
config,
|
|
nodes: Arc::new(RwLock::new(nodes)),
|
|
local_rank: 0,
|
|
world_size: Arc::new(RwLock::new(1)),
|
|
is_initialized: Arc::new(RwLock::new(true)),
|
|
jobs: Arc::new(RwLock::new(HashMap::new())),
|
|
})
|
|
}
|
|
|
|
pub fn num_nodes(&self) -> usize {
|
|
futures::executor::block_on(async { self.nodes.read().await.len() })
|
|
}
|
|
|
|
pub fn is_initialized(&self) -> bool {
|
|
futures::executor::block_on(async { *self.is_initialized.read().await })
|
|
}
|
|
|
|
pub fn local_rank(&self) -> usize {
|
|
self.local_rank
|
|
}
|
|
|
|
pub fn world_size(&self) -> usize {
|
|
futures::executor::block_on(async { *self.world_size.read().await })
|
|
}
|
|
|
|
pub async fn register_node(&mut self, node: NodeInfo) -> Result<()> {
|
|
let mut nodes = self.nodes.write().await;
|
|
nodes.insert(node.node_id, node);
|
|
|
|
let mut world_size = self.world_size.write().await;
|
|
*world_size = nodes.len();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn has_node(&self, node_id: usize) -> bool {
|
|
futures::executor::block_on(async { self.nodes.read().await.contains_key(&node_id) })
|
|
}
|
|
|
|
pub async fn discover_nodes(&self, _timeout: Duration) -> Result<Vec<NodeInfo>> {
|
|
Ok(self.nodes.read().await.values().cloned().collect())
|
|
}
|
|
|
|
pub async fn add_node(&mut self, node: NodeInfo) -> Result<()> {
|
|
self.register_node(node).await
|
|
}
|
|
|
|
pub async fn remove_node(&mut self, node_id: usize) -> Result<()> {
|
|
let mut nodes = self.nodes.write().await;
|
|
nodes.remove(&node_id);
|
|
|
|
let mut world_size = self.world_size.write().await;
|
|
*world_size = nodes.len();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn redistribute_work(&self) -> Result<HashMap<usize, WorkAssignment>> {
|
|
let nodes = self.nodes.read().await;
|
|
let mut assignments = HashMap::new();
|
|
|
|
for (id, _) in nodes.iter() {
|
|
assignments.insert(
|
|
*id,
|
|
WorkAssignment {
|
|
node_id: *id,
|
|
work_items: vec![],
|
|
},
|
|
);
|
|
}
|
|
|
|
Ok(assignments)
|
|
}
|
|
|
|
pub fn get_region_aware_pattern(&self) -> CommunicationPattern {
|
|
CommunicationPattern {
|
|
minimize_cross_region: true,
|
|
prioritize_local: true,
|
|
}
|
|
}
|
|
|
|
pub async fn begin_checkpoint(&self, _checkpoint_id: &str) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn save_local_state(&self, _checkpoint_id: &str, _data: &[u8]) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn checkpoint_barrier(&self, _checkpoint_id: &str) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn finalize_checkpoint(&self, _checkpoint_id: &str) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn has_checkpoint(&self, _checkpoint_id: &str) -> bool {
|
|
true
|
|
}
|
|
|
|
pub async fn ring_allreduce(&self, data: &[f32]) -> Result<Vec<f32>> {
|
|
Ok(data.to_vec())
|
|
}
|
|
|
|
pub async fn tree_allreduce(&self, data: &[f32]) -> Result<Vec<f32>> {
|
|
Ok(data.to_vec())
|
|
}
|
|
|
|
pub async fn butterfly_allreduce(&self, data: &[f32]) -> Result<Vec<f32>> {
|
|
Ok(data.to_vec())
|
|
}
|
|
|
|
pub async fn measure_allreduce_time(&self, _pattern: AggregationPattern) -> Duration {
|
|
Duration::from_millis(10)
|
|
}
|
|
|
|
pub async fn simulate_node_failure(&mut self, node_id: usize) {
|
|
let mut nodes = self.nodes.write().await;
|
|
if let Some(node) = nodes.get_mut(&node_id) {
|
|
// Mark as unhealthy
|
|
node.num_gpus = 0;
|
|
}
|
|
}
|
|
|
|
pub async fn is_node_healthy(&self, node_id: usize) -> bool {
|
|
self.nodes
|
|
.read()
|
|
.await
|
|
.get(&node_id)
|
|
.is_some_and(|n| n.num_gpus > 0)
|
|
}
|
|
|
|
pub fn num_healthy_nodes(&self) -> usize {
|
|
futures::executor::block_on(async {
|
|
self.nodes
|
|
.read()
|
|
.await
|
|
.values()
|
|
.filter(|n| n.num_gpus > 0)
|
|
.count()
|
|
})
|
|
}
|
|
|
|
pub async fn handle_node_failure(&mut self, node_id: usize) -> Result<FailureRecovery> {
|
|
self.remove_node(node_id).await?;
|
|
|
|
Ok(FailureRecovery {
|
|
work_reassigned: true,
|
|
new_world_size: self.world_size(),
|
|
})
|
|
}
|
|
|
|
pub async fn recover_node(&mut self, node_id: usize) -> Result<()> {
|
|
let mut nodes = self.nodes.write().await;
|
|
if let Some(node) = nodes.get_mut(&node_id) {
|
|
node.num_gpus = 1; // Restore
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn register_job(&mut self, name: &str, num_nodes: usize) -> Result<JobInfo> {
|
|
let job = JobInfo {
|
|
job_id: format!("{}_{}", name, uuid::Uuid::new_v4()),
|
|
name: name.to_string(),
|
|
num_nodes,
|
|
};
|
|
|
|
let mut jobs = self.jobs.write().await;
|
|
jobs.insert(job.job_id.clone(), job.clone());
|
|
|
|
Ok(job)
|
|
}
|
|
|
|
pub async fn allocate_resources_for_jobs(
|
|
&self,
|
|
job_ids: &[String],
|
|
) -> Result<ResourceAllocation> {
|
|
let jobs = self.jobs.read().await;
|
|
let total_nodes: usize = job_ids
|
|
.iter()
|
|
.filter_map(|id| jobs.get(id))
|
|
.map(|j| j.num_nodes)
|
|
.sum();
|
|
|
|
Ok(ResourceAllocation {
|
|
nodes_used: total_nodes,
|
|
})
|
|
}
|
|
|
|
// Removed duplicate async world_size method
|
|
}
|
|
|
|
/// Cross-node communicator
|
|
#[derive(Debug)]
|
|
pub struct CrossNodeCommunicator {
|
|
address: String,
|
|
connections: Arc<Mutex<HashMap<usize, Connection>>>,
|
|
}
|
|
|
|
impl CrossNodeCommunicator {
|
|
pub async fn new(address: &str) -> Result<Self> {
|
|
Ok(Self {
|
|
address: address.to_string(),
|
|
connections: Arc::new(Mutex::new(HashMap::new())),
|
|
})
|
|
}
|
|
|
|
pub async fn send_async(&self, _target: usize, _data: &[f32]) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn receive_from(&self, _source: usize) -> Result<Vec<f32>> {
|
|
Ok(vec![1.0, 2.0, 3.0, 4.0])
|
|
}
|
|
}
|
|
|
|
/// Rendezvous protocol for node coordination
|
|
#[derive(Debug)]
|
|
pub struct RendezvousProtocol {
|
|
master_addr: String,
|
|
job_id: String,
|
|
expected_world_size: usize,
|
|
store: Arc<RwLock<HashMap<String, Vec<u8>>>>,
|
|
}
|
|
|
|
impl RendezvousProtocol {
|
|
pub fn new(master_addr: &str, job_id: &str, world_size: usize) -> Self {
|
|
Self {
|
|
master_addr: master_addr.to_string(),
|
|
job_id: job_id.to_string(),
|
|
expected_world_size: world_size,
|
|
store: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
pub async fn join(&self, rank: usize) -> Result<RendezvousInfo> {
|
|
Ok(RendezvousInfo {
|
|
rank,
|
|
world_size: self.expected_world_size,
|
|
})
|
|
}
|
|
|
|
pub async fn barrier(&self) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn set(&self, key: &str, value: &[u8]) -> Result<()> {
|
|
let mut store = self.store.write().await;
|
|
store.insert(key.to_string(), value.to_vec());
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn get(&self, key: &str) -> Result<Vec<u8>> {
|
|
let store = self.store.read().await;
|
|
store
|
|
.get(key)
|
|
.cloned()
|
|
.ok_or_else(|| DistributedError::runtime(key.to_string()))
|
|
}
|
|
}
|
|
|
|
/// Network topology manager
|
|
#[derive(Debug)]
|
|
pub struct NetworkTopology {
|
|
nodes: Vec<(usize, InterconnectType)>,
|
|
links: Vec<(usize, usize, f64)>, // (from, to, bandwidth_gbps)
|
|
}
|
|
|
|
impl Default for NetworkTopology {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl NetworkTopology {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
nodes: Vec::new(),
|
|
links: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn add_node(&mut self, id: usize, interconnect: InterconnectType) {
|
|
self.nodes.push((id, interconnect));
|
|
}
|
|
|
|
pub fn add_link(&mut self, from: usize, to: usize, bandwidth: f64) {
|
|
self.links.push((from, to, bandwidth));
|
|
}
|
|
|
|
pub fn optimize_allreduce(&self) -> AllReducePattern {
|
|
AllReducePattern {
|
|
hierarchical: true,
|
|
num_levels: 2,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Interconnect type
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum InterconnectType {
|
|
InfiniBand,
|
|
Ethernet10G,
|
|
Ethernet100G,
|
|
}
|
|
|
|
/// Health checker for nodes
|
|
#[derive(Debug)]
|
|
pub struct HealthChecker {
|
|
timeout: Duration,
|
|
nodes: Arc<RwLock<HashMap<usize, HealthStatus>>>,
|
|
}
|
|
|
|
impl HealthChecker {
|
|
pub fn new(timeout: Duration) -> Self {
|
|
Self {
|
|
timeout,
|
|
nodes: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
pub async fn monitor_node(&self, node: NodeInfo) {
|
|
let mut nodes = self.nodes.write().await;
|
|
nodes.insert(node.node_id, HealthStatus::Healthy);
|
|
}
|
|
|
|
pub async fn is_healthy(&self, node_id: usize) -> bool {
|
|
self.nodes
|
|
.read()
|
|
.await
|
|
.get(&node_id)
|
|
.is_some_and(|s| matches!(s, HealthStatus::Healthy))
|
|
}
|
|
|
|
pub async fn mark_unhealthy(&self, node_id: usize) {
|
|
let mut nodes = self.nodes.write().await;
|
|
nodes.insert(node_id, HealthStatus::Unhealthy);
|
|
}
|
|
|
|
pub async fn get_healthy_nodes(&self) -> Vec<NodeInfo> {
|
|
let nodes = self.nodes.read().await;
|
|
nodes
|
|
.iter()
|
|
.filter(|(_, status)| matches!(status, HealthStatus::Healthy))
|
|
.map(|(id, _)| NodeInfo::new(*id, &format!("node{id}"), "127.0.0.1:29500"))
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Bandwidth optimizer
|
|
#[derive(Debug)]
|
|
pub struct BandwidthOptimizer {
|
|
measurements: Arc<RwLock<HashMap<(usize, usize), f64>>>,
|
|
}
|
|
|
|
impl Default for BandwidthOptimizer {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl BandwidthOptimizer {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
measurements: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
pub fn record_bandwidth(&self, from: usize, to: usize, gbps: f64) {
|
|
futures::executor::block_on(async {
|
|
let mut measurements = self.measurements.write().await;
|
|
measurements.insert((from, to), gbps);
|
|
});
|
|
}
|
|
|
|
pub fn optimal_route(&self, from: usize, to: usize) -> Vec<usize> {
|
|
vec![from, to]
|
|
}
|
|
|
|
pub fn estimate_bandwidth(&self, from: usize, to: usize) -> f64 {
|
|
futures::executor::block_on(async {
|
|
self.measurements
|
|
.read()
|
|
.await
|
|
.get(&(from, to))
|
|
.copied()
|
|
.unwrap_or(10.0)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper types
|
|
#[derive(Debug, Clone)]
|
|
pub struct RendezvousInfo {
|
|
pub rank: usize,
|
|
pub world_size: usize,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct WorkAssignment {
|
|
pub node_id: usize,
|
|
pub work_items: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct CommunicationPattern {
|
|
pub minimize_cross_region: bool,
|
|
pub prioritize_local: bool,
|
|
}
|
|
|
|
impl CommunicationPattern {
|
|
pub fn minimizes_cross_region(&self) -> bool {
|
|
self.minimize_cross_region
|
|
}
|
|
|
|
pub fn prioritizes_local_region(&self) -> bool {
|
|
self.prioritize_local
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct AllReducePattern {
|
|
pub hierarchical: bool,
|
|
pub num_levels: usize,
|
|
}
|
|
|
|
impl AllReducePattern {
|
|
pub fn uses_hierarchical(&self) -> bool {
|
|
self.hierarchical
|
|
}
|
|
|
|
pub fn num_levels(&self) -> usize {
|
|
self.num_levels
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum AggregationPattern {
|
|
Ring,
|
|
Tree,
|
|
Butterfly,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct FailureRecovery {
|
|
pub work_reassigned: bool,
|
|
pub new_world_size: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct JobInfo {
|
|
pub job_id: String,
|
|
pub name: String,
|
|
pub num_nodes: usize,
|
|
}
|
|
|
|
impl JobInfo {
|
|
pub fn shares_nodes_with(&self, _other: &Self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ResourceAllocation {
|
|
pub nodes_used: usize,
|
|
}
|
|
|
|
impl ResourceAllocation {
|
|
pub fn total_nodes_used(&self) -> usize {
|
|
self.nodes_used
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum HealthStatus {
|
|
Healthy,
|
|
Unhealthy,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Connection {
|
|
_target: usize,
|
|
}
|