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

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,
}