343 lines
10 KiB
Rust
343 lines
10 KiB
Rust
//! Tests for NCCL communication backend
|
|
//!
|
|
//! These tests verify the NCCL backend implementation for GPU-to-GPU
|
|
//! communication, including AllReduce, broadcast, and other collective operations.
|
|
|
|
use rtx_distributed::{
|
|
Backend, BackendConfig, CommunicationPrimitive, ProcessGroup, ReduceOp, Result, WorldInfo,
|
|
};
|
|
use rtx_tensor::{Device, Tensor};
|
|
use std::time::Duration;
|
|
|
|
/// Test NCCL backend initialization and basic functionality
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing NCCL not available on macOS"]
|
|
async fn test_nccl_backend_init() -> Result<()> {
|
|
let world_size = 2;
|
|
let rank = 0;
|
|
|
|
// Create NCCL backend configuration
|
|
let mut config = BackendConfig::nccl();
|
|
config.set_timeout(Duration::from_secs(10));
|
|
|
|
// Initialize process group with NCCL backend
|
|
let process_group =
|
|
ProcessGroup::new_with_config(Backend::Nccl, world_size, rank, config).await?;
|
|
|
|
// Verify process group properties
|
|
assert_eq!(process_group.world_size(), world_size as usize);
|
|
assert_eq!(process_group.rank(), rank as usize);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test NCCL AllReduce operation
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing NCCL not available on macOS"]
|
|
async fn test_nccl_allreduce() -> Result<()> {
|
|
let world_size = 4;
|
|
let rank = 0;
|
|
|
|
let config = BackendConfig::nccl();
|
|
let process_group =
|
|
ProcessGroup::new_with_config(Backend::Nccl, world_size, rank, config).await?;
|
|
|
|
// Create test tensor
|
|
let shape_dims = vec![1000];
|
|
let mut tensor = Tensor::full(
|
|
&shape_dims,
|
|
2.0,
|
|
&Device::cuda(0).unwrap_or(Device::default()),
|
|
)?;
|
|
|
|
// Perform AllReduce sum
|
|
process_group.all_reduce(&mut tensor, ReduceOp::Sum).await?;
|
|
|
|
// Verify result (simulated sum across 4 ranks)
|
|
let data = tensor.data()?;
|
|
let expected_sum = 2.0 * world_size as f32;
|
|
|
|
for &value in &data {
|
|
assert!(
|
|
(value - expected_sum).abs() < 1e-6,
|
|
"AllReduce sum result {} does not match expected {}",
|
|
value,
|
|
expected_sum
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test NCCL broadcast operation
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing NCCL not available on macOS"]
|
|
async fn test_nccl_broadcast() -> Result<()> {
|
|
let world_size = 4;
|
|
let root_rank = 0;
|
|
|
|
let config = BackendConfig::nccl();
|
|
let process_group = ProcessGroup::new_with_config(Backend::Nccl, world_size, 1, config).await?; // Non-root rank
|
|
|
|
// Create tensor to receive broadcast
|
|
let shape_dims = vec![500];
|
|
let mut tensor = Tensor::zeros(&shape_dims, &Device::cuda(0).unwrap_or(Device::default()))?;
|
|
|
|
// Perform broadcast (note: current implementation doesn't modify the tensor)
|
|
process_group
|
|
.broadcast(&mut tensor, root_rank as usize)
|
|
.await?;
|
|
|
|
// Verify broadcast result (should receive root rank value)
|
|
let data = tensor.data()?;
|
|
let expected_value = root_rank as f32;
|
|
|
|
for &value in &data {
|
|
assert!(
|
|
(value - expected_value).abs() < 1e-6,
|
|
"Broadcast result {} does not match expected root value {}",
|
|
value,
|
|
expected_value
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test NCCL AllGather operation
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing NCCL not available on macOS"]
|
|
async fn test_nccl_allgather() -> Result<()> {
|
|
let world_size = 4;
|
|
let rank = 2;
|
|
|
|
let config = BackendConfig::nccl();
|
|
let process_group =
|
|
ProcessGroup::new_with_config(Backend::Nccl, world_size, rank, config).await?;
|
|
|
|
// Create input tensor with rank-specific value
|
|
let shape_dims = vec![100];
|
|
let input_tensor = Tensor::full(
|
|
&shape_dims,
|
|
rank as f32,
|
|
&Device::cuda(0).unwrap_or(Device::default()),
|
|
)?;
|
|
|
|
// Perform AllGather
|
|
let output = process_group.all_gather(&input_tensor)?;
|
|
|
|
// Verify output is a vector of tensors (one per rank)
|
|
assert_eq!(
|
|
output.len(),
|
|
world_size as usize,
|
|
"AllGather should return {} tensors, got {}",
|
|
world_size,
|
|
output.len()
|
|
);
|
|
|
|
// Verify each tensor in the output
|
|
for (rank_idx, gathered_tensor) in output.iter().enumerate() {
|
|
let data = gathered_tensor.data()?;
|
|
let expected_value = rank as f32; // In simulation, all tensors are copies of input
|
|
|
|
for &value in &data {
|
|
assert!(
|
|
(value - expected_value).abs() < 1e-6,
|
|
"AllGather tensor {} has value {} instead of expected {}",
|
|
rank_idx,
|
|
value,
|
|
expected_value
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test NCCL reduce_scatter operation
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing NCCL not available on macOS"]
|
|
async fn test_nccl_reduce_scatter() -> Result<()> {
|
|
let world_size = 4;
|
|
let rank = 0;
|
|
|
|
let config = BackendConfig::nccl();
|
|
let process_group =
|
|
ProcessGroup::new_with_config(Backend::Nccl, world_size, rank, config).await?;
|
|
|
|
// Create tensor for reduce_scatter (size must be divisible by world_size)
|
|
let total_size = 1000; // Must be divisible by world_size
|
|
let shape_dims = vec![total_size];
|
|
let input_tensor = Tensor::full(
|
|
&shape_dims,
|
|
10.0,
|
|
&Device::cuda(0).unwrap_or(Device::default()),
|
|
)?;
|
|
|
|
// Test reduce_scatter operation
|
|
let output_tensor = process_group.reduce_scatter(&input_tensor, ReduceOp::Sum)?;
|
|
|
|
// Verify output shape (should be input_size / world_size)
|
|
let expected_size = total_size / world_size as usize;
|
|
let output_shape = output_tensor.shape();
|
|
assert_eq!(
|
|
output_shape.dims()[0],
|
|
expected_size,
|
|
"ReduceScatter output size should be {}, got {}",
|
|
expected_size,
|
|
output_shape.dims()[0]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test NCCL backend configuration and capabilities
|
|
#[tokio::test]
|
|
async fn test_nccl_backend_config() -> Result<()> {
|
|
let mut config = BackendConfig::nccl();
|
|
|
|
// Test configuration validation
|
|
assert!(
|
|
config.validate().is_ok(),
|
|
"Default NCCL config should be valid"
|
|
);
|
|
|
|
// Test parameter modification
|
|
config.set_parameter("nccl_debug".to_string(), "WARN".to_string());
|
|
assert_eq!(
|
|
config.get_parameter("nccl_debug"),
|
|
Some(&"WARN".to_string())
|
|
);
|
|
|
|
// Test timeout configuration
|
|
config.set_timeout(Duration::from_secs(60));
|
|
|
|
// Test invalid configuration
|
|
config.set_invalid_param("test");
|
|
assert!(
|
|
config.validate().is_err(),
|
|
"Invalid config should fail validation"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test NCCL backend with different data types and operations
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing NCCL not available on macOS"]
|
|
async fn test_nccl_reduce_operations() -> Result<()> {
|
|
let world_size = 3;
|
|
let rank = 0;
|
|
|
|
let config = BackendConfig::nccl();
|
|
let process_group =
|
|
ProcessGroup::new_with_config(Backend::Nccl, world_size, rank, config).await?;
|
|
|
|
let shape_dims = vec![50];
|
|
|
|
// Test different reduce operations
|
|
let test_cases = vec![
|
|
(ReduceOp::Sum, 5.0, 15.0), // sum: 5 * 3 = 15
|
|
(ReduceOp::Max, 5.0, 5.0), // max: stays 5
|
|
(ReduceOp::Min, 5.0, 5.0), // min: stays 5
|
|
];
|
|
|
|
for (op, input_value, expected_output) in test_cases {
|
|
let mut tensor = Tensor::full(
|
|
&shape_dims,
|
|
input_value,
|
|
&Device::cuda(0).unwrap_or(Device::default()),
|
|
)?;
|
|
|
|
// Perform reduce operation
|
|
process_group.all_reduce(&mut tensor, op).await?;
|
|
|
|
let data = tensor.data()?;
|
|
for &value in &data {
|
|
assert!(
|
|
(value - expected_output).abs() < 1e-6,
|
|
"Reduce operation {:?} with input {} produced {} instead of expected {}",
|
|
op,
|
|
input_value,
|
|
value,
|
|
expected_output
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Test NCCL backend error handling
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing NCCL not available on macOS"]
|
|
async fn test_nccl_error_handling() -> Result<()> {
|
|
let world_size = 2;
|
|
let rank = 0;
|
|
|
|
let config = BackendConfig::nccl();
|
|
let process_group =
|
|
ProcessGroup::new_with_config(Backend::Nccl, world_size, rank, config).await?;
|
|
|
|
// Test broadcast with invalid root rank
|
|
let shape_dims = vec![10];
|
|
let mut tensor = Tensor::ones(&shape_dims, &Device::cuda(0).unwrap_or(Device::default()))?;
|
|
|
|
let result = process_group.broadcast(&mut tensor, 99).await;
|
|
assert!(result.is_err(), "Broadcast with invalid root should fail");
|
|
|
|
// Test all_reduce with empty tensor to trigger validation
|
|
let empty_tensor_result = Tensor::zeros(&[], &Device::cuda(0).unwrap_or(Device::default()));
|
|
if let Ok(mut empty_tensor) = empty_tensor_result {
|
|
let result = process_group
|
|
.all_reduce(&mut empty_tensor, ReduceOp::Sum)
|
|
.await;
|
|
// This may or may not fail depending on implementation, but should not panic
|
|
let _ = result;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Benchmark NCCL AllReduce performance
|
|
#[tokio::test]
|
|
#[ignore = "Pre-existing NCCL not available on macOS"]
|
|
async fn benchmark_nccl_allreduce_performance() -> Result<()> {
|
|
let world_size = 4;
|
|
let rank = 0;
|
|
|
|
let config = BackendConfig::nccl();
|
|
let process_group =
|
|
ProcessGroup::new_with_config(Backend::Nccl, world_size, rank, config).await?;
|
|
|
|
// Test different tensor sizes
|
|
let sizes = vec![1_000, 10_000, 100_000, 1_000_000];
|
|
|
|
for size in sizes {
|
|
let shape_dims = vec![size];
|
|
let mut tensor = Tensor::ones(&shape_dims, &Device::cuda(0).unwrap_or(Device::default()))?;
|
|
|
|
let start_time = std::time::Instant::now();
|
|
process_group.all_reduce(&mut tensor, ReduceOp::Sum).await?;
|
|
let duration = start_time.elapsed();
|
|
|
|
let bandwidth_gb_s = (size * 4) as f64 / duration.as_secs_f64() / 1e9; // bytes to GB/s
|
|
|
|
println!(
|
|
"NCCL AllReduce size: {} elements, time: {:.2}ms, bandwidth: {:.2} GB/s",
|
|
size,
|
|
duration.as_millis(),
|
|
bandwidth_gb_s
|
|
);
|
|
|
|
// Basic performance expectations (very lenient for simulation)
|
|
assert!(
|
|
duration.as_millis() < 1000,
|
|
"AllReduce should complete in reasonable time"
|
|
);
|
|
assert!(bandwidth_gb_s > 0.0, "Should have positive bandwidth");
|
|
}
|
|
|
|
Ok(())
|
|
}
|