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

1052 lines
30 KiB
Rust

//! Hybrid Parallelism Coordinator
//!
//! This module coordinates multiple parallelism strategies together:
//! - Data Parallelism (DP): Replicate model, partition data
//! - Tensor Parallelism (TP): Partition tensors within layers
//! - Pipeline Parallelism (PP): Partition layers across stages
//! - Sequence Parallelism (SP): Partition along sequence dimension
//!
//! Hybrid parallelism enables training models that are too large for
//! single GPU memory while maximizing throughput.
use crate::error::{DistributedError, Result};
use parking_lot::{Mutex, RwLock};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{Duration, Instant};
// =============================================================================
// Configuration
// =============================================================================
/// Parallelism dimension type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ParallelDimension {
/// Data parallelism
Data,
/// Tensor parallelism
Tensor,
/// Pipeline parallelism
Pipeline,
/// Sequence parallelism
Sequence,
/// Expert parallelism (for MoE)
Expert,
}
/// Configuration for hybrid parallelism
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HybridParallelConfig {
/// Total number of GPUs
pub total_gpus: usize,
/// Data parallel degree
pub dp_degree: usize,
/// Tensor parallel degree
pub tp_degree: usize,
/// Pipeline parallel degree
pub pp_degree: usize,
/// Sequence parallel degree (usually equals tp_degree)
pub sp_degree: usize,
/// Expert parallel degree (for MoE models)
pub ep_degree: usize,
/// Enable sequence parallelism
pub enable_sequence_parallel: bool,
/// Enable activation checkpointing
pub enable_checkpointing: bool,
/// Number of micro-batches for pipeline
pub num_micro_batches: usize,
/// Overlap communication with computation
pub overlap_comm: bool,
}
impl Default for HybridParallelConfig {
fn default() -> Self {
Self {
total_gpus: 8,
dp_degree: 2,
tp_degree: 2,
pp_degree: 2,
sp_degree: 1,
ep_degree: 1,
enable_sequence_parallel: false,
enable_checkpointing: true,
num_micro_batches: 4,
overlap_comm: true,
}
}
}
impl HybridParallelConfig {
/// Validate configuration
pub fn validate(&self) -> Result<()> {
let product = self.dp_degree * self.tp_degree * self.pp_degree;
if product != self.total_gpus {
return Err(DistributedError::configuration(format!(
"DP({}) x TP({}) x PP({}) = {} must equal total_gpus ({})",
self.dp_degree, self.tp_degree, self.pp_degree, product, self.total_gpus
)));
}
if self.enable_sequence_parallel && self.sp_degree != self.tp_degree {
return Err(DistributedError::configuration(
"Sequence parallel degree must equal tensor parallel degree",
));
}
Ok(())
}
/// Create for data parallelism only
pub fn data_parallel(num_gpus: usize) -> Self {
Self {
total_gpus: num_gpus,
dp_degree: num_gpus,
tp_degree: 1,
pp_degree: 1,
sp_degree: 1,
..Default::default()
}
}
/// Create for tensor + data parallelism
pub fn tensor_data_parallel(tp_degree: usize, dp_degree: usize) -> Self {
Self {
total_gpus: tp_degree * dp_degree,
dp_degree,
tp_degree,
pp_degree: 1,
sp_degree: 1,
..Default::default()
}
}
/// Create for 3D parallelism
pub fn parallelism_3d(dp_degree: usize, tp_degree: usize, pp_degree: usize) -> Self {
Self {
total_gpus: dp_degree * tp_degree * pp_degree,
dp_degree,
tp_degree,
pp_degree,
sp_degree: 1,
..Default::default()
}
}
}
// =============================================================================
// Process Group Mesh
// =============================================================================
/// Represents a GPU in the parallel mesh
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MeshCoordinate {
/// Global rank (0 to total_gpus - 1)
pub global_rank: usize,
/// Data parallel rank
pub dp_rank: usize,
/// Tensor parallel rank
pub tp_rank: usize,
/// Pipeline parallel rank (stage)
pub pp_rank: usize,
/// Device ID
pub device_id: i32,
}
impl MeshCoordinate {
/// Check if this is the first TP rank
pub fn is_tp_first(&self) -> bool {
self.tp_rank == 0
}
/// Check if this is the last TP rank
pub fn is_tp_last(&self, tp_degree: usize) -> bool {
self.tp_rank == tp_degree - 1
}
/// Check if this is the first PP stage
pub fn is_pp_first(&self) -> bool {
self.pp_rank == 0
}
/// Check if this is the last PP stage
pub fn is_pp_last(&self, pp_degree: usize) -> bool {
self.pp_rank == pp_degree - 1
}
}
/// Process group for a subset of GPUs
#[derive(Debug, Clone)]
pub struct ProcessGroupInfo {
/// Group name
pub name: String,
/// Parallelism dimension
pub dimension: ParallelDimension,
/// Ranks in this group
pub ranks: Vec<usize>,
/// This process's rank within the group
pub local_rank: usize,
/// Group size
pub size: usize,
}
impl ProcessGroupInfo {
/// Create a new process group info
pub fn new(
name: String,
dimension: ParallelDimension,
ranks: Vec<usize>,
local_rank: usize,
) -> Self {
let size = ranks.len();
Self {
name,
dimension,
ranks,
local_rank,
size,
}
}
/// Check if rank is in this group
pub fn contains(&self, rank: usize) -> bool {
self.ranks.contains(&rank)
}
}
/// Manages the GPU mesh for hybrid parallelism
pub struct ProcessGroupMesh {
/// Configuration
config: HybridParallelConfig,
/// This process's global rank
global_rank: usize,
/// Mesh coordinate for this process
coordinate: MeshCoordinate,
/// Data parallel group
dp_group: ProcessGroupInfo,
/// Tensor parallel group
tp_group: ProcessGroupInfo,
/// Pipeline parallel group
pp_group: ProcessGroupInfo,
/// All process groups by name
groups: HashMap<String, ProcessGroupInfo>,
}
impl ProcessGroupMesh {
/// Create a new process group mesh
pub fn new(config: HybridParallelConfig, global_rank: usize) -> Result<Self> {
config.validate()?;
if global_rank >= config.total_gpus {
return Err(DistributedError::configuration(format!(
"Global rank {} exceeds total GPUs {}",
global_rank, config.total_gpus
)));
}
// Calculate mesh coordinates
// Layout: [DP, TP, PP] with PP varying fastest
let pp_rank = global_rank % config.pp_degree;
let tp_rank = (global_rank / config.pp_degree) % config.tp_degree;
let dp_rank = global_rank / (config.pp_degree * config.tp_degree);
let coordinate = MeshCoordinate {
global_rank,
dp_rank,
tp_rank,
pp_rank,
device_id: global_rank as i32,
};
// Build process groups
let dp_group = Self::build_dp_group(&config, &coordinate);
let tp_group = Self::build_tp_group(&config, &coordinate);
let pp_group = Self::build_pp_group(&config, &coordinate);
let mut groups = HashMap::new();
groups.insert(dp_group.name.clone(), dp_group.clone());
groups.insert(tp_group.name.clone(), tp_group.clone());
groups.insert(pp_group.name.clone(), pp_group.clone());
Ok(Self {
config,
global_rank,
coordinate,
dp_group,
tp_group,
pp_group,
groups,
})
}
/// Build data parallel group (same TP and PP ranks)
fn build_dp_group(config: &HybridParallelConfig, coord: &MeshCoordinate) -> ProcessGroupInfo {
let mut ranks = Vec::new();
for dp in 0..config.dp_degree {
let rank = dp * config.pp_degree * config.tp_degree
+ coord.tp_rank * config.pp_degree
+ coord.pp_rank;
ranks.push(rank);
}
ProcessGroupInfo::new(
format!("dp_tp{}_pp{}", coord.tp_rank, coord.pp_rank),
ParallelDimension::Data,
ranks,
coord.dp_rank,
)
}
/// Build tensor parallel group (same DP and PP ranks)
fn build_tp_group(config: &HybridParallelConfig, coord: &MeshCoordinate) -> ProcessGroupInfo {
let mut ranks = Vec::new();
for tp in 0..config.tp_degree {
let rank = coord.dp_rank * config.pp_degree * config.tp_degree
+ tp * config.pp_degree
+ coord.pp_rank;
ranks.push(rank);
}
ProcessGroupInfo::new(
format!("tp_dp{}_pp{}", coord.dp_rank, coord.pp_rank),
ParallelDimension::Tensor,
ranks,
coord.tp_rank,
)
}
/// Build pipeline parallel group (same DP and TP ranks)
fn build_pp_group(config: &HybridParallelConfig, coord: &MeshCoordinate) -> ProcessGroupInfo {
let mut ranks = Vec::new();
for pp in 0..config.pp_degree {
let rank = coord.dp_rank * config.pp_degree * config.tp_degree
+ coord.tp_rank * config.pp_degree
+ pp;
ranks.push(rank);
}
ProcessGroupInfo::new(
format!("pp_dp{}_tp{}", coord.dp_rank, coord.tp_rank),
ParallelDimension::Pipeline,
ranks,
coord.pp_rank,
)
}
/// Get mesh coordinate
pub fn coordinate(&self) -> &MeshCoordinate {
&self.coordinate
}
/// Get data parallel group
pub fn dp_group(&self) -> &ProcessGroupInfo {
&self.dp_group
}
/// Get tensor parallel group
pub fn tp_group(&self) -> &ProcessGroupInfo {
&self.tp_group
}
/// Get pipeline parallel group
pub fn pp_group(&self) -> &ProcessGroupInfo {
&self.pp_group
}
/// Get group by name
pub fn get_group(&self, name: &str) -> Option<&ProcessGroupInfo> {
self.groups.get(name)
}
/// Get all group names
pub fn group_names(&self) -> Vec<String> {
self.groups.keys().cloned().collect()
}
/// Get global rank
pub fn global_rank(&self) -> usize {
self.global_rank
}
/// Get config
pub fn config(&self) -> &HybridParallelConfig {
&self.config
}
/// Check if this rank should participate in data parallel AllReduce
pub fn should_dp_allreduce(&self) -> bool {
self.config.dp_degree > 1
}
/// Check if this rank should participate in tensor parallel AllReduce
pub fn should_tp_allreduce(&self) -> bool {
self.config.tp_degree > 1
}
/// Get the next rank in pipeline (for send)
pub fn next_pp_rank(&self) -> Option<usize> {
if self.coordinate.pp_rank < self.config.pp_degree - 1 {
Some(self.pp_group.ranks[self.coordinate.pp_rank + 1])
} else {
None
}
}
/// Get the previous rank in pipeline (for recv)
pub fn prev_pp_rank(&self) -> Option<usize> {
if self.coordinate.pp_rank > 0 {
Some(self.pp_group.ranks[self.coordinate.pp_rank - 1])
} else {
None
}
}
}
// =============================================================================
// Hybrid Parallelism Coordinator
// =============================================================================
/// Coordinates hybrid parallelism for distributed training
pub struct HybridParallelCoordinator {
/// Configuration
config: HybridParallelConfig,
/// Process group mesh
mesh: ProcessGroupMesh,
/// Current training step
step: AtomicU64,
/// Is training active
active: AtomicBool,
/// Pending communications
pending_comms: Mutex<Vec<PendingComm>>,
/// Statistics
stats: RwLock<CoordinatorStats>,
}
/// A pending communication
#[derive(Debug, Clone)]
pub struct PendingComm {
/// Communication ID
pub id: u64,
/// Operation type
pub op: CommOp,
/// Parallelism dimension
pub dimension: ParallelDimension,
/// Source rank
pub src: usize,
/// Destination ranks
pub dst: Vec<usize>,
/// Data size in bytes
pub size_bytes: usize,
/// Creation time
pub created_at: Instant,
}
/// Communication operation type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommOp {
/// AllReduce for gradient synchronization
AllReduce,
/// AllGather for tensor parallel
AllGather,
/// ReduceScatter for FSDP
ReduceScatter,
/// Send to next pipeline stage
Send,
/// Recv from previous pipeline stage
Recv,
/// Broadcast
Broadcast,
}
impl HybridParallelCoordinator {
/// Create a new hybrid parallelism coordinator
pub fn new(config: HybridParallelConfig, global_rank: usize) -> Result<Self> {
let mesh = ProcessGroupMesh::new(config.clone(), global_rank)?;
Ok(Self {
config,
mesh,
step: AtomicU64::new(0),
active: AtomicBool::new(false),
pending_comms: Mutex::new(Vec::new()),
stats: RwLock::new(CoordinatorStats::default()),
})
}
/// Start training
pub fn start(&self) {
self.active.store(true, Ordering::SeqCst);
}
/// Stop training
pub fn stop(&self) {
self.active.store(false, Ordering::SeqCst);
}
/// Check if active
pub fn is_active(&self) -> bool {
self.active.load(Ordering::SeqCst)
}
/// Advance to next step
pub fn next_step(&self) -> u64 {
self.step.fetch_add(1, Ordering::SeqCst) + 1
}
/// Get current step
pub fn current_step(&self) -> u64 {
self.step.load(Ordering::SeqCst)
}
/// Schedule gradient AllReduce across data parallel group
pub fn schedule_dp_allreduce(&self, _name: &str, size_bytes: usize) -> Option<u64> {
if !self.mesh.should_dp_allreduce() {
return None;
}
let group = self.mesh.dp_group();
let id = self.create_pending_comm(
CommOp::AllReduce,
ParallelDimension::Data,
group.ranks.clone(),
size_bytes,
);
let mut stats = self.stats.write();
stats.dp_allreduce_count += 1;
stats.dp_allreduce_bytes += size_bytes;
Some(id)
}
/// Schedule tensor parallel AllGather
pub fn schedule_tp_allgather(&self, _name: &str, size_bytes: usize) -> Option<u64> {
if !self.mesh.should_tp_allreduce() {
return None;
}
let group = self.mesh.tp_group();
let id = self.create_pending_comm(
CommOp::AllGather,
ParallelDimension::Tensor,
group.ranks.clone(),
size_bytes,
);
let mut stats = self.stats.write();
stats.tp_allgather_count += 1;
stats.tp_allgather_bytes += size_bytes;
Some(id)
}
/// Schedule tensor parallel ReduceScatter
pub fn schedule_tp_reduce_scatter(&self, _name: &str, size_bytes: usize) -> Option<u64> {
if !self.mesh.should_tp_allreduce() {
return None;
}
let group = self.mesh.tp_group();
let id = self.create_pending_comm(
CommOp::ReduceScatter,
ParallelDimension::Tensor,
group.ranks.clone(),
size_bytes,
);
Some(id)
}
/// Schedule pipeline send
pub fn schedule_pp_send(&self, size_bytes: usize) -> Option<u64> {
let next_rank = self.mesh.next_pp_rank()?;
let id = self.create_pending_comm(
CommOp::Send,
ParallelDimension::Pipeline,
vec![next_rank],
size_bytes,
);
let mut stats = self.stats.write();
stats.pp_send_count += 1;
stats.pp_send_bytes += size_bytes;
Some(id)
}
/// Schedule pipeline recv
pub fn schedule_pp_recv(&self, size_bytes: usize) -> Option<u64> {
let prev_rank = self.mesh.prev_pp_rank()?;
let id = self.create_pending_comm(
CommOp::Recv,
ParallelDimension::Pipeline,
vec![prev_rank],
size_bytes,
);
let mut stats = self.stats.write();
stats.pp_recv_count += 1;
stats.pp_recv_bytes += size_bytes;
Some(id)
}
/// Create a pending communication
fn create_pending_comm(
&self,
op: CommOp,
dimension: ParallelDimension,
dst: Vec<usize>,
size_bytes: usize,
) -> u64 {
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
let comm = PendingComm {
id,
op,
dimension,
src: self.mesh.global_rank(),
dst,
size_bytes,
created_at: Instant::now(),
};
self.pending_comms.lock().push(comm);
id
}
/// Get pending communication count
pub fn pending_count(&self) -> usize {
self.pending_comms.lock().len()
}
/// Clear pending communications
pub fn clear_pending(&self) {
self.pending_comms.lock().clear();
}
/// Get mesh
pub fn mesh(&self) -> &ProcessGroupMesh {
&self.mesh
}
/// Get configuration
pub fn config(&self) -> &HybridParallelConfig {
&self.config
}
/// Get statistics
pub fn stats(&self) -> CoordinatorStats {
self.stats.read().clone()
}
/// Get parallelism summary
pub fn parallelism_summary(&self) -> String {
format!(
"DP={} x TP={} x PP={} (Total: {} GPUs)",
self.config.dp_degree,
self.config.tp_degree,
self.config.pp_degree,
self.config.total_gpus
)
}
}
// =============================================================================
// Coordinator Statistics
// =============================================================================
/// Statistics for hybrid parallelism coordinator
#[derive(Debug, Default, Clone)]
pub struct CoordinatorStats {
/// Data parallel AllReduce count
pub dp_allreduce_count: usize,
/// Data parallel AllReduce bytes
pub dp_allreduce_bytes: usize,
/// Tensor parallel AllGather count
pub tp_allgather_count: usize,
/// Tensor parallel AllGather bytes
pub tp_allgather_bytes: usize,
/// Pipeline send count
pub pp_send_count: usize,
/// Pipeline send bytes
pub pp_send_bytes: usize,
/// Pipeline recv count
pub pp_recv_count: usize,
/// Pipeline recv bytes
pub pp_recv_bytes: usize,
/// Total communication time
pub total_comm_time: Duration,
}
impl CoordinatorStats {
/// Get total communication bytes
pub fn total_bytes(&self) -> usize {
self.dp_allreduce_bytes + self.tp_allgather_bytes + self.pp_send_bytes + self.pp_recv_bytes
}
/// Get total operation count
pub fn total_ops(&self) -> usize {
self.dp_allreduce_count + self.tp_allgather_count + self.pp_send_count + self.pp_recv_count
}
}
// =============================================================================
// Thread-Safe Wrappers
// =============================================================================
/// Thread-safe shared hybrid parallel coordinator
pub type SharedHybridParallelCoordinator = Arc<HybridParallelCoordinator>;
/// Create a shared hybrid parallel coordinator
pub fn shared_hybrid_coordinator(
config: HybridParallelConfig,
global_rank: usize,
) -> Result<SharedHybridParallelCoordinator> {
Ok(Arc::new(HybridParallelCoordinator::new(
config,
global_rank,
)?))
}
// =============================================================================
// Tests
// =============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hybrid_config_default() {
let config = HybridParallelConfig::default();
assert_eq!(config.total_gpus, 8);
assert_eq!(config.dp_degree, 2);
assert_eq!(config.tp_degree, 2);
assert_eq!(config.pp_degree, 2);
}
#[test]
fn test_config_validate_success() {
let config = HybridParallelConfig {
total_gpus: 8,
dp_degree: 2,
tp_degree: 2,
pp_degree: 2,
..Default::default()
};
assert!(config.validate().is_ok());
}
#[test]
fn test_config_validate_failure() {
let config = HybridParallelConfig {
total_gpus: 8,
dp_degree: 2,
tp_degree: 2,
pp_degree: 3, // 2 x 2 x 3 = 12 != 8
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_data_parallel_config() {
let config = HybridParallelConfig::data_parallel(8);
assert_eq!(config.dp_degree, 8);
assert_eq!(config.tp_degree, 1);
assert_eq!(config.pp_degree, 1);
assert!(config.validate().is_ok());
}
#[test]
fn test_tensor_data_parallel_config() {
let config = HybridParallelConfig::tensor_data_parallel(4, 2);
assert_eq!(config.total_gpus, 8);
assert_eq!(config.dp_degree, 2);
assert_eq!(config.tp_degree, 4);
assert!(config.validate().is_ok());
}
#[test]
fn test_3d_parallelism_config() {
let config = HybridParallelConfig::parallelism_3d(2, 2, 2);
assert_eq!(config.total_gpus, 8);
assert!(config.validate().is_ok());
}
#[test]
fn test_mesh_coordinate() {
let config = HybridParallelConfig::default();
let mesh = ProcessGroupMesh::new(config, 0).unwrap();
let coord = mesh.coordinate();
assert_eq!(coord.global_rank, 0);
assert_eq!(coord.dp_rank, 0);
assert_eq!(coord.tp_rank, 0);
assert_eq!(coord.pp_rank, 0);
}
#[test]
fn test_mesh_coordinate_rank_5() {
// Layout: [DP, TP, PP] with PP varying fastest
// Rank 5 = 5 in base [2,2,2]
// pp = 5 % 2 = 1
// tp = (5 / 2) % 2 = 0
// dp = 5 / 4 = 1
let config = HybridParallelConfig::default();
let mesh = ProcessGroupMesh::new(config, 5).unwrap();
let coord = mesh.coordinate();
assert_eq!(coord.global_rank, 5);
assert_eq!(coord.pp_rank, 1);
assert_eq!(coord.tp_rank, 0);
assert_eq!(coord.dp_rank, 1);
}
#[test]
fn test_dp_group() {
let config = HybridParallelConfig::default();
let mesh = ProcessGroupMesh::new(config, 0).unwrap();
let dp_group = mesh.dp_group();
assert_eq!(dp_group.dimension, ParallelDimension::Data);
assert_eq!(dp_group.size, 2); // dp_degree = 2
assert_eq!(dp_group.local_rank, 0);
}
#[test]
fn test_tp_group() {
let config = HybridParallelConfig::default();
let mesh = ProcessGroupMesh::new(config, 0).unwrap();
let tp_group = mesh.tp_group();
assert_eq!(tp_group.dimension, ParallelDimension::Tensor);
assert_eq!(tp_group.size, 2); // tp_degree = 2
}
#[test]
fn test_pp_group() {
let config = HybridParallelConfig::default();
let mesh = ProcessGroupMesh::new(config, 0).unwrap();
let pp_group = mesh.pp_group();
assert_eq!(pp_group.dimension, ParallelDimension::Pipeline);
assert_eq!(pp_group.size, 2); // pp_degree = 2
}
#[test]
fn test_pp_next_prev() {
let config = HybridParallelConfig::default();
// First stage
let mesh_0 = ProcessGroupMesh::new(config.clone(), 0).unwrap();
assert!(mesh_0.next_pp_rank().is_some());
assert!(mesh_0.prev_pp_rank().is_none());
// Last stage
let mesh_1 = ProcessGroupMesh::new(config, 1).unwrap();
assert!(mesh_1.next_pp_rank().is_none());
assert!(mesh_1.prev_pp_rank().is_some());
}
#[test]
fn test_should_allreduce() {
let config = HybridParallelConfig::default();
let mesh = ProcessGroupMesh::new(config, 0).unwrap();
assert!(mesh.should_dp_allreduce()); // dp_degree = 2
assert!(mesh.should_tp_allreduce()); // tp_degree = 2
let dp_only = HybridParallelConfig::data_parallel(4);
let mesh_dp = ProcessGroupMesh::new(dp_only, 0).unwrap();
assert!(mesh_dp.should_dp_allreduce());
assert!(!mesh_dp.should_tp_allreduce()); // tp_degree = 1
}
#[test]
fn test_coordinator_creation() {
let config = HybridParallelConfig::default();
let coord = HybridParallelCoordinator::new(config, 0).unwrap();
assert!(!coord.is_active());
assert_eq!(coord.current_step(), 0);
assert_eq!(coord.pending_count(), 0);
}
#[test]
fn test_coordinator_start_stop() {
let config = HybridParallelConfig::default();
let coord = HybridParallelCoordinator::new(config, 0).unwrap();
coord.start();
assert!(coord.is_active());
coord.stop();
assert!(!coord.is_active());
}
#[test]
fn test_coordinator_steps() {
let config = HybridParallelConfig::default();
let coord = HybridParallelCoordinator::new(config, 0).unwrap();
assert_eq!(coord.current_step(), 0);
assert_eq!(coord.next_step(), 1);
assert_eq!(coord.next_step(), 2);
assert_eq!(coord.current_step(), 2);
}
#[test]
fn test_schedule_dp_allreduce() {
let config = HybridParallelConfig::default();
let coord = HybridParallelCoordinator::new(config, 0).unwrap();
let id = coord.schedule_dp_allreduce("gradients", 1024);
assert!(id.is_some());
assert_eq!(coord.pending_count(), 1);
let stats = coord.stats();
assert_eq!(stats.dp_allreduce_count, 1);
assert_eq!(stats.dp_allreduce_bytes, 1024);
}
#[test]
fn test_schedule_tp_allgather() {
let config = HybridParallelConfig::default();
let coord = HybridParallelCoordinator::new(config, 0).unwrap();
let id = coord.schedule_tp_allgather("weights", 2048);
assert!(id.is_some());
assert_eq!(coord.pending_count(), 1);
}
#[test]
fn test_schedule_pp_send() {
let config = HybridParallelConfig::default();
// Rank 0 is first stage, can send
let coord = HybridParallelCoordinator::new(config, 0).unwrap();
let id = coord.schedule_pp_send(512);
assert!(id.is_some());
}
#[test]
fn test_schedule_pp_recv() {
let config = HybridParallelConfig::default();
// Rank 1 is second stage, can recv
let coord = HybridParallelCoordinator::new(config, 1).unwrap();
let id = coord.schedule_pp_recv(512);
assert!(id.is_some());
}
#[test]
fn test_parallelism_summary() {
let config = HybridParallelConfig::default();
let coord = HybridParallelCoordinator::new(config, 0).unwrap();
let summary = coord.parallelism_summary();
assert!(summary.contains("DP=2"));
assert!(summary.contains("TP=2"));
assert!(summary.contains("PP=2"));
}
#[test]
fn test_clear_pending() {
let config = HybridParallelConfig::default();
let coord = HybridParallelCoordinator::new(config, 0).unwrap();
coord.schedule_dp_allreduce("g1", 100);
coord.schedule_dp_allreduce("g2", 200);
assert_eq!(coord.pending_count(), 2);
coord.clear_pending();
assert_eq!(coord.pending_count(), 0);
}
#[test]
fn test_coordinator_stats() {
let stats = CoordinatorStats {
dp_allreduce_bytes: 1000,
tp_allgather_bytes: 500,
pp_send_bytes: 200,
pp_recv_bytes: 200,
dp_allreduce_count: 2,
tp_allgather_count: 1,
pp_send_count: 1,
pp_recv_count: 1,
..Default::default()
};
assert_eq!(stats.total_bytes(), 1900);
assert_eq!(stats.total_ops(), 5);
}
#[test]
fn test_shared_coordinator() {
let config = HybridParallelConfig::default();
let coord = shared_hybrid_coordinator(config, 0).unwrap();
assert_eq!(coord.mesh().global_rank(), 0);
}
#[test]
fn test_mesh_coordinate_helpers() {
let coord = MeshCoordinate {
global_rank: 0,
dp_rank: 0,
tp_rank: 0,
pp_rank: 0,
device_id: 0,
};
assert!(coord.is_tp_first());
assert!(coord.is_pp_first());
assert!(!coord.is_tp_last(2));
assert!(!coord.is_pp_last(2));
}
#[test]
fn test_process_group_info_contains() {
let group = ProcessGroupInfo::new(
"test".to_string(),
ParallelDimension::Data,
vec![0, 2, 4, 6],
0,
);
assert!(group.contains(0));
assert!(group.contains(4));
assert!(!group.contains(1));
assert!(!group.contains(3));
}
#[test]
fn test_groups_cover_all_ranks() {
let config = HybridParallelConfig::default();
// For each rank, check that all parallel groups are valid
for rank in 0..8 {
let mesh = ProcessGroupMesh::new(config.clone(), rank).unwrap();
assert!(mesh.dp_group().contains(rank));
assert!(mesh.tp_group().contains(rank));
assert!(mesh.pp_group().contains(rank));
}
}
#[test]
fn test_no_dp_allreduce_for_single_dp() {
let config = HybridParallelConfig::data_parallel(1);
let coord = HybridParallelCoordinator::new(config, 0).unwrap();
// With dp_degree=1, no AllReduce should be scheduled
let id = coord.schedule_dp_allreduce("grad", 100);
assert!(id.is_none());
}
}