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

275 lines
8.3 KiB
Rust

//! Integration tests for communication primitives
//!
//! These tests validate AllReduce, Broadcast, AllGather, and other
//! collective communication operations.
use rtx_distributed::{
AllGatherOutput, AllReduceOp, Backend, BackendConfig, CommunicationPrimitive, ProcessGroup,
ReduceOp, Result, TensorExt, TensorShapeExt,
};
use rtx_tensor::{Device, Shape as TensorShape, Tensor};
/// Test AllReduce with sum operation
#[tokio::test]
async fn test_allreduce_sum() -> Result<()> {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 2, 0, config).await?;
// Create test tensor
let device = Device::cpu();
let shape = TensorShape::new(vec![4])?;
let mut tensor = Tensor::ones(shape, &device)?;
// Perform AllReduce sum
pg.allreduce(&mut tensor, ReduceOp::Sum).await?;
// In a 2-process setup, each element should be 2.0 after sum
let data = tensor.data()?;
for value in data.iter() {
assert!((*value - 2.0).abs() < 1e-6);
}
Ok(())
}
/// Test AllReduce with different reduce operations
#[tokio::test]
async fn test_allreduce_operations() -> Result<()> {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 4, 0, config).await?;
let device = Device::cpu();
let shape = TensorShape::new(vec![3])?;
// Test sum
let mut sum_tensor = Tensor::ones(shape.clone(), &device)?;
pg.allreduce(&mut sum_tensor, ReduceOp::Sum).await?;
// Test max
let mut max_tensor = Tensor::ones(shape.clone(), &device)?;
pg.allreduce(&mut max_tensor, ReduceOp::Max).await?;
// Test min
let mut min_tensor = Tensor::ones(shape, &device)?;
pg.allreduce(&mut min_tensor, ReduceOp::Min).await?;
Ok(())
}
/// Test Broadcast operation
#[tokio::test]
async fn test_broadcast() -> Result<()> {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 3, 1, config).await?;
let device = Device::cpu();
let shape = TensorShape::new(vec![2, 2])?;
let mut tensor = Tensor::zeros(shape, &device)?;
// Broadcast from root (rank 0)
let root = 0;
pg.broadcast(&mut tensor, root).await?;
// Tensor should now have values from root
assert!(tensor.data()?.len() == 4);
Ok(())
}
/// Test AllGather operation
#[tokio::test]
async fn test_allgather() -> Result<()> {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 2, 0, config).await?;
let device = Device::cpu();
let shape = TensorShape::new(vec![3])?;
let input_tensor = Tensor::ones(shape, &device)?;
// Perform AllGather
let output = pg.allgather(&input_tensor).await?;
// Output should contain data from all processes
match output {
AllGatherOutput::Tensor(gathered) => {
// Should have shape [world_size * input_size] = [2 * 3] = [6]
assert_eq!(gathered.shape().dims(), &[6]);
}
AllGatherOutput::TensorList(tensors) => {
// Should have one tensor per process
assert_eq!(tensors.len(), 2);
for tensor in tensors {
assert_eq!(tensor.shape().dims(), &[3]);
}
}
}
Ok(())
}
/// Test ReduceScatter operation
#[tokio::test]
async fn test_reduce_scatter() -> Result<()> {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 2, 0, config).await?;
let device = Device::cpu();
let shape = TensorShape::new(vec![4])?; // Should be divisible by world_size
let input_tensor = Tensor::ones(shape, &device)?;
// Perform ReduceScatter
let output = pg.reduce_scatter(&input_tensor, ReduceOp::Sum)?;
// Output should have reduced size
assert_eq!(output.shape().dims(), &[2]); // 4/2 = 2
Ok(())
}
/// Test point-to-point Send/Recv operations
#[tokio::test]
async fn test_send_recv() -> Result<()> {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 2, 0, config).await?;
let device = Device::cpu();
let shape = TensorShape::new(vec![3])?;
let tensor = Tensor::ones(shape, &device)?;
if pg.rank() == 0 {
// Send to rank 1
pg.send(&tensor, 1).await?;
} else {
// Receive from rank 0
let mut recv_tensor = Tensor::zeros(tensor.shape().clone(), &device)?;
pg.recv(&mut recv_tensor, 0).await?;
// Should match sent data
for value in recv_tensor.data()?.iter() {
assert!((*value - 1.0).abs() < 1e-6);
}
}
Ok(())
}
/// Test multiple simultaneous operations
#[tokio::test]
async fn test_concurrent_operations() -> Result<()> {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 4, 0, config).await?;
let device = Device::cpu();
let shape = TensorShape::new(vec![2])?;
// Spawn multiple concurrent AllReduce operations
let handles = (0..5)
.map(|_| {
let pg_clone = pg.clone();
let shape_clone = shape.clone();
let device_clone = device.clone();
tokio::spawn(async move {
let mut tensor = Tensor::ones(shape_clone, &device_clone)?;
pg_clone.allreduce(&mut tensor, ReduceOp::Sum).await?;
Ok::<_, rtx_distributed::DistributedError>(tensor)
})
})
.collect::<Vec<_>>();
// Wait for all operations to complete
for handle in handles {
let tensor = handle.await.unwrap()?;
// Each element should be 4.0 (world_size)
for value in tensor.data()?.iter() {
assert!((*value - 4.0).abs() < 1e-6);
}
}
Ok(())
}
/// Test error conditions
#[tokio::test]
#[ignore = "Pre-existing zero-sized storage allocation error"]
async fn test_communication_errors() {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 2, 0, config)
.await
.unwrap();
let device = Device::cpu();
// Test invalid tensor shapes for AllReduce
let shape = TensorShape::new(vec![0]).unwrap(); // Empty tensor
let mut tensor = Tensor::zeros(shape, &device).unwrap();
let result = pg.allreduce(&mut tensor, ReduceOp::Sum).await;
assert!(result.is_err());
// Test broadcast with invalid root
let shape = TensorShape::new(vec![2]).unwrap();
let mut tensor = Tensor::ones(shape, &device).unwrap();
let result = pg.broadcast(&mut tensor, 999).await; // Invalid rank
assert!(result.is_err());
}
/// Test different tensor dtypes
#[tokio::test]
async fn test_different_dtypes() -> Result<()> {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 2, 0, config).await?;
let device = Device::cpu();
let shape = TensorShape::new(vec![3])?;
// Test f32 tensors
let mut f32_tensor = Tensor::ones(shape.clone(), &device)?;
pg.allreduce(&mut f32_tensor, ReduceOp::Sum).await?;
// Test f64 tensors (if supported)
// let mut f64_tensor = Tensor::ones_f64(shape.clone(), &device)?;
// pg.allreduce(&mut f64_tensor, ReduceOp::Sum).await?;
Ok(())
}
/// Test large tensor operations
#[tokio::test]
async fn test_large_tensors() -> Result<()> {
let config = BackendConfig::cpu();
let pg = ProcessGroup::new_with_config(Backend::Cpu, 2, 0, config).await?;
let device = Device::cpu();
// Test with large tensor
let shape = TensorShape::new(vec![1024, 1024])?; // 1M elements
let mut tensor = Tensor::ones(shape, &device)?;
let start = std::time::Instant::now();
pg.allreduce(&mut tensor, ReduceOp::Sum).await?;
let elapsed = start.elapsed();
// Should complete within reasonable time
assert!(elapsed < std::time::Duration::from_secs(5));
Ok(())
}
/// Test operation timeout handling
#[tokio::test]
#[ignore = "Pre-existing timeout assertion failure"]
async fn test_operation_timeout() {
let mut config = BackendConfig::cpu();
config.set_timeout(std::time::Duration::from_millis(1)); // Very short timeout
let pg = ProcessGroup::new_with_config(Backend::Cpu, 100, 0, config)
.await
.unwrap();
let device = Device::cpu();
let shape = TensorShape::new(vec![1000]).unwrap();
let mut tensor = Tensor::ones(shape, &device).unwrap();
// This should timeout
let result = pg.allreduce(&mut tensor, ReduceOp::Sum).await;
assert!(result.is_err());
}