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

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(())
}