142860442a
* Added MPI support to execute unit/functional tests
Update node and process validation
Updated node detection count and modified validation method
Update validation logic to include max procs and nodes
* Address review comments
* Fix warnings
* Added a new NET transport test and clean up
* Added MPI test logging mechanism
* Decoupled GTest framework
* Added Net IB functional tests
* Updated with resource guards
* Added NET IB tests and refactored code
* Update P2pWorkflow test
* Update documentation
* Add MPI_TESTS_ENABLED guard to the file
* Fix Shm and NetIB tests
* Applied refactoring and cleanup
* Replaced BufferGuard with AutoGuard
* Modified test debug logging
* Use macro to reduce NcclTypeTraits code duplication
- Replace repetitive template specializations with a single
DEFINE_NCCL_TYPE_TRAIT macro
- Use stringification operator (#) to auto-generate type name strings
- Add #undef to keep macro from polluting namespace
- Makes adding new type mappings trivial
* Unify buffer initialization with generic pattern function
- Remove initializeBufferWithCustomPattern
- Make initializeBufferWithPattern generic with PatternFunc template param
- Now single function handles all patterns via lambda injection
- Updated all test files to use lambdas for pattern generation
- Pattern logic now visible at call site (self-documenting)
* Unify buffer verification with pluggable pattern function
- Remove verifyBufferWithCustomCheck
- Make verifyBufferData generic with PatternFunc template param
- Single function handles all verification patterns via lambda injection
- Updated all test files to use lambdas
- Better defaults: num_samples=0 means verify all elements
- Pattern logic now visible at call site (self-documenting)
* Docs: Add DeviceBufferHelpers section to MPITestRunner.md
- Document new refactored buffer initialization/verification API
- Explain pluggable pattern functions with lambda examples
- Show type mapping and automatic float/int comparison
- Include migration guide from old API to new unified functions
- Demonstrate best practices with real-world examples
- Reference recent refactoring commits (macro-based type traits)
* Docs: Update documentation and examples
- Update on DeviceBufferHelpers
- Update examples using DeviceBufferHelpers methods, e.g. data verification
* Address review comment.
- Replace manual pattern generation loop with initializeBufferWithPattern call
- Use downloadBuffer to get host copy instead of manual hipMemcpy
* Remove non-existent dependency
* Remove duplicate testcase
* Code cleanup in test files
* Moved common constants to base class
[ROCm/rccl commit: 29e1567b95]
2615 行
81 KiB
Markdown
2615 行
81 KiB
Markdown
# MPI Test Runner (Google Test)
|
|
|
|
A simple C++ testing framework for multi-process RCCL tests using MPI (Message Passing Interface) and **Google Test**.
|
|
|
|
> **📝 Note:** This guide mostly covers **Google Test-based** MPI testing. For standalone tests refer to [Standalone Tests](#standalone-tests).
|
|
|
|
## Table of Contents
|
|
- [Overview](#overview)
|
|
- [Why Use MPI Testing?](#why-use-mpi-testing)
|
|
- [Quick Start](#quick-start)
|
|
- [Core Concepts](#core-concepts)
|
|
- [Per-Rank Logging](#per-rank-logging)
|
|
- [API Reference](#api-reference)
|
|
- [Examples](#examples)
|
|
- [Best Practices](#best-practices)
|
|
- [RAII Resource Guards](#raii-resource-guards)
|
|
- [Device Buffer Helpers](#device-buffer-helpers)
|
|
- [Troubleshooting](#troubleshooting)
|
|
- [Standalone Tests](#standalone-tests)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
`MPITestBase` is a Google Test adapter for writing multi-process tests that verify RCCL features across multiple GPUs. It provides infrastructure for MPI-based distributed testing.
|
|
|
|
**Key Features:**
|
|
- ✅ Multi-process testing with MPI
|
|
- ✅ Automatic RCCL communicator management
|
|
- ✅ Process count validation (minimum processes, power-of-two requirements)
|
|
- ✅ Node count validation (single-node vs multi-node constraints)
|
|
- ✅ HIP stream lifecycle management
|
|
- ✅ Integrated with Google Test framework
|
|
- ✅ Test-specific communicators for isolation
|
|
- ✅ RAII guards for automatic resource cleanup
|
|
- ✅ Pluggable buffer helpers with lambda patterns
|
|
- ✅ Framework-agnostic core (can be used without GTest)
|
|
|
|
**Locations:**
|
|
- `test/common/MPITestBase.hpp` - Main test infrastructure
|
|
- `test/common/ResourceGuards.hpp` - RAII guards
|
|
- `test/common/DeviceBufferHelpers.hpp` - Buffer utilities
|
|
|
|
---
|
|
|
|
## Why Use MPI Testing?
|
|
|
|
### Problem: Single-Process Testing is Insufficient
|
|
|
|
RCCL (ROCm Communication Collectives Library) is designed for **multi-GPU, multi-node** communication. Testing these features requires:
|
|
|
|
1. **Multiple Processes** - Each GPU/rank runs in its own process
|
|
2. **Distributed Coordination** - Synchronization across processes
|
|
3. **Real Communication** - Actual data transfer between GPUs
|
|
4. **Collective Operations** - AllReduce, Broadcast, etc. require all ranks
|
|
|
|
**Example: Testing AllReduce**
|
|
|
|
```cpp
|
|
// ❌ CANNOT meaningfully test this with single process
|
|
void testAllReduce() {
|
|
// AllReduce requires ALL ranks to participate
|
|
ncclAllReduce(send, recv, count, ncclFloat, ncclSum, comm, stream);
|
|
|
|
// You need multiple processes to test actual collective behavior!
|
|
}
|
|
```
|
|
|
|
**With MPI Testing:**
|
|
```cpp
|
|
// ✅ CAN test with multiple MPI processes
|
|
TEST_F(MyMPITest, AllReduce) {
|
|
// Rank 0: sends value 1.0
|
|
// Rank 1: sends value 2.0
|
|
// Rank 2: sends value 3.0
|
|
// All ranks receive: 6.0 (sum)
|
|
|
|
ncclAllReduce(send, recv, count, ncclFloat, ncclSum, comm, stream);
|
|
|
|
// Each rank can verify the result
|
|
EXPECT_EQ(result, 6.0f);
|
|
}
|
|
```
|
|
|
|
### Common Use Cases
|
|
|
|
1. **Collective Operations** - AllReduce, Broadcast, AllGather, ReduceScatter
|
|
2. **Point-to-Point Communication** - Send/Recv between specific ranks
|
|
3. **Transport Layer Testing** - P2P, SHM (shared memory), NET (network) transports
|
|
4. **Multi-Node Scenarios** - Cross-node communication
|
|
5. **Scalability Testing** - Behavior with different numbers of processes
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
### Prerequisites
|
|
|
|
1. **MPI Implementation** - OpenMPI, MPICH, or similar
|
|
2. **Multiple GPUs** - At least 2 GPUs for most tests
|
|
3. **Build with MPI Support** - `MPI_TESTS_ENABLED` must be defined
|
|
|
|
### Basic Example
|
|
|
|
```cpp
|
|
#include "MPITestBase.hpp"
|
|
#include "ResourceGuards.hpp"
|
|
|
|
// Import constants and guards
|
|
using namespace MPITestConstants;
|
|
using namespace RCCLTestGuards;
|
|
|
|
class MyMPITest : public MPITestBase {
|
|
protected:
|
|
void SetUp() override {
|
|
// Optional: Add custom setup
|
|
}
|
|
};
|
|
|
|
TEST_F(MyMPITest, BasicAllReduce) {
|
|
// Validate we have enough processes (uses defaults for other parameters)
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI)); // min=2, no max, any nodes
|
|
|
|
// Create test-specific communicator
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
const int N = 1024;
|
|
float* d_send = nullptr;
|
|
float* d_recv = nullptr;
|
|
|
|
// Allocate GPU memory with RAII guards for automatic cleanup
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&d_send, N * sizeof(float)));
|
|
auto send_guard = makeDeviceBufferAutoGuard(d_send);
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&d_recv, N * sizeof(float)));
|
|
auto recv_guard = makeDeviceBufferAutoGuard(d_recv);
|
|
|
|
// Initialize with rank-specific data
|
|
float value = MPIEnvironment::world_rank + 1.0f;
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMemset(d_send, value, N * sizeof(float)));
|
|
|
|
// Perform AllReduce
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclAllReduce(
|
|
d_send, d_recv, N, ncclFloat, ncclSum,
|
|
getActiveCommunicator(),
|
|
getActiveStream()
|
|
));
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(getActiveStream()));
|
|
|
|
// Verify result
|
|
std::vector<float> result(N);
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMemcpy(result.data(), d_recv, N * sizeof(float),
|
|
hipMemcpyDeviceToHost));
|
|
|
|
float expected = (MPIEnvironment::world_size *
|
|
(MPIEnvironment::world_size + 1)) / 2.0f;
|
|
|
|
for (int i = 0; i < N; i++) {
|
|
EXPECT_FLOAT_EQ(result[i], expected);
|
|
}
|
|
|
|
// Automatic cleanup via RAII guards - no manual hipFree() needed!
|
|
}
|
|
```
|
|
|
|
### Running MPI Tests
|
|
|
|
```bash
|
|
# Run with 2 processes on single node
|
|
mpirun -np 2 ./test_executable --gtest_filter="*MPI*"
|
|
|
|
# Run with 4 processes on single node
|
|
mpirun -np 4 ./test_executable --gtest_filter="MyMPITest.*"
|
|
|
|
# Run with specific GPU mapping on single node
|
|
mpirun -np 2 --bind-to none -x HIP_VISIBLE_DEVICES=0,1 ./test_executable
|
|
|
|
# Run across multiple nodes using hostfile (recommended)
|
|
# Create hostfile with slots per node:
|
|
cat > hostfile.txt << EOF
|
|
node-1 slots=8
|
|
node-2 slots=8
|
|
EOF
|
|
mpirun -np 16 --hostfile hostfile.txt ./test_executable
|
|
|
|
# Or let SLURM auto-generate hostfile (if using test runner script)
|
|
salloc -N 2 -n 16 --time=01:00:00
|
|
./build_test_coverage.std.sh --config test_config.txt --no-build
|
|
# Script automatically detects nodes and creates temporary hostfile
|
|
```
|
|
|
|
**Important Notes:**
|
|
- **CPU binding disabled**: Tests run with `--bind-to none` to avoid "more processes than CPUs" errors
|
|
- **GPU assignment**: Each node independently assigns GPUs 0-N to local ranks 0-N
|
|
- **Multi-node**: Script auto-generates hostfiles with proper slot counts from SLURM allocations
|
|
|
|
**Important: Node Validation**
|
|
|
|
Tests can specify node requirements to ensure they run in the correct environment:
|
|
|
|
| Node Requirement | Constant | Use Case Examples |
|
|
|------------------|----------|-------------------|
|
|
| Single-node only | `kRequireSingleNode` | Tests requiring direct GPU access, shared memory, or specific hardware topology |
|
|
| Any number of nodes | `kNoNodeLimit` (default) | Tests designed for distributed execution or network-based features |
|
|
|
|
```cpp
|
|
// Test that requires single-node execution
|
|
validateTestPrerequisites(2, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kRequireSingleNode);
|
|
|
|
// Test that works on any number of nodes (default - can omit last parameters)
|
|
validateTestPrerequisites(2); // Uses defaults: no max processes, any nodes
|
|
```
|
|
|
|
**Common Use Cases:**
|
|
- **Single-node requirement**: P2P transport, SHM transport, GPU topology tests, local memory tests
|
|
- **Multi-node capable**: NET transport, distributed collectives, scalability tests
|
|
|
|
---
|
|
|
|
## Core Concepts
|
|
|
|
### 1. MPIEnvironment
|
|
|
|
Global MPI environment that manages MPI initialization and cleanup.
|
|
|
|
**Static Members:**
|
|
```cpp
|
|
MPIEnvironment::world_rank // Current process rank (0 to N-1)
|
|
MPIEnvironment::world_size // Total number of processes
|
|
MPIEnvironment::retCode // Initialization status (0 = success)
|
|
```
|
|
|
|
**Lifecycle:**
|
|
- Initialized once before any tests run
|
|
- Calls `MPI_Init()` and sets up GPU-to-rank mapping
|
|
- Cleaned up after all tests complete
|
|
- Each rank is assigned to a unique GPU **based on local rank** (rank within node)
|
|
|
|
**Multi-Node GPU Assignment:**
|
|
For multi-node configurations, GPU assignment uses **local rank** (rank within the node) rather than global rank:
|
|
- **Node 1**: Global ranks 0-7 → Local ranks 0-7 → GPUs 0-7
|
|
- **Node 2**: Global ranks 8-15 → Local ranks 0-7 → GPUs 0-7
|
|
|
|
This ensures proper GPU mapping across all nodes without requiring unique GPU IDs globally.
|
|
|
|
**Process Distribution Display:**
|
|
At startup, the framework automatically displays detailed process distribution:
|
|
```
|
|
=== MPI Process Distribution ===
|
|
Total processes: 16
|
|
Detected nodes: 2
|
|
|
|
Node 0: node-3 (8 ranks)
|
|
Ranks: 0, 1, 2, 3, 4, 5, 6, 7
|
|
Node 1: node-21 (8 ranks)
|
|
Ranks: 8, 9, 10, 11, 12, 13, 14, 15
|
|
================================
|
|
```
|
|
|
|
This helps verify correct process placement and node allocation before tests run.
|
|
|
|
### 2. MPITestBase Class
|
|
|
|
Base class providing common MPI test infrastructure.
|
|
|
|
**Key Methods:**
|
|
```cpp
|
|
class MPITestBase : public ::testing::Test {
|
|
protected:
|
|
// Validate process and node count requirements
|
|
bool validateTestPrerequisites(int min_processes = 1,
|
|
int max_processes = kNoProcessLimit,
|
|
bool require_power_of_two = false,
|
|
int min_nodes = 1,
|
|
int max_nodes = kNoNodeLimit);
|
|
|
|
// Create isolated communicator for this test
|
|
ncclResult_t createTestCommunicator();
|
|
|
|
// Get the active communicator
|
|
ncclComm_t getActiveCommunicator();
|
|
|
|
// Get the active HIP stream
|
|
hipStream_t getActiveStream();
|
|
|
|
// Cleanup resources (called automatically)
|
|
void cleanupTestCommunicator();
|
|
};
|
|
```
|
|
|
|
### 3. Test-Specific Communicators
|
|
|
|
Each test can create its own RCCL communicator for isolation:
|
|
|
|
```cpp
|
|
TEST_F(MyTest, IsolatedTest) {
|
|
// Create a fresh communicator just for this test
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
// Use it for operations
|
|
ncclComm_t comm = getActiveCommunicator();
|
|
hipStream_t stream = getActiveStream();
|
|
|
|
// Automatically cleaned up in TearDown()
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- Tests don't interfere with each other
|
|
- Clean state for each test
|
|
- Proper resource cleanup
|
|
- No shared memory conflicts
|
|
|
|
### 4. Process and Node Validation
|
|
|
|
Ensure tests have the right number of processes and correct node configuration:
|
|
|
|
```cpp
|
|
// Require at least 2 processes (uses defaults for other parameters)
|
|
validateTestPrerequisites(2); // min=2, no max, not power-of-two, any nodes
|
|
|
|
// Require at least 4 processes
|
|
validateTestPrerequisites(4);
|
|
|
|
// Require power-of-two processes (2, 4, 8, 16, ...)
|
|
validateTestPrerequisites(2, kNoProcessLimit, kRequirePowerOfTwo);
|
|
|
|
// Require exactly 2 processes (min=2, max=2)
|
|
validateTestPrerequisites(2, 2); // Uses defaults for other parameters
|
|
|
|
// Test that requires single-node (e.g., P2P transport, shared memory tests)
|
|
validateTestPrerequisites(2, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kRequireSingleNode);
|
|
|
|
// Test that requires at least 2 nodes
|
|
validateTestPrerequisites(4, kNoProcessLimit, kNoPowerOfTwoRequired, 2);
|
|
|
|
// Test with 4-16 processes, power-of-two, single-node only
|
|
validateTestPrerequisites(4, 16, kRequirePowerOfTwo, 1, kRequireSingleNode);
|
|
```
|
|
|
|
**Node Detection:**
|
|
The framework automatically detects the number of unique nodes using `MPI_Comm_split_type()` with `MPI_COMM_TYPE_SHARED`, which groups ranks by shared memory domain (physical node).
|
|
|
|
**The MPI Process Distribution, Test Requirements, and Current Environment are displayed for ALL tests** (when `NCCL_DEBUG=INFO`), providing visibility into your actual MPI configuration regardless of node constraints.
|
|
|
|
**Important:** Node detection reports WHERE processes are actually running, not where you intended them to run. If your `mpirun` command doesn't properly distribute processes across nodes (missing hostfile, missing `--host`, or missing distribution policy like `--map-by ppr:N:node`), all processes will launch on the local node and detection will correctly report 1 node.
|
|
|
|
**For multi-node testing, you MUST:**
|
|
- Use `srun` with SLURM allocations (automatically distributes), OR
|
|
- Provide hostfile: `mpirun --hostfile hostfile.txt`, OR
|
|
- Specify hosts: `mpirun --host node1:N,node2:N`, OR
|
|
- Use distribution policy: `mpirun --map-by ppr:N:node`
|
|
|
|
Without proper process distribution, `mpirun -np 16` launches all 16 processes on the local node, and node detection correctly reports 1 node.
|
|
|
|
**When to Use Node Validation:**
|
|
- Use `kRequireSingleNode` when your test requires all processes to be on the same physical node
|
|
- Use `kNoNodeLimit` (default) when your test can work across multiple nodes
|
|
- The validation automatically skips tests that don't meet node requirements
|
|
|
|
### 5. Synchronization
|
|
|
|
MPI barriers ensure all ranks reach certain points together:
|
|
|
|
```cpp
|
|
// Explicit barrier (use sparingly)
|
|
ASSERT_MPI_SUCCESS(MPI_Barrier(MPI_COMM_WORLD));
|
|
|
|
// Barriers are automatically used in:
|
|
// - createTestCommunicator() (before and after)
|
|
// - cleanupTestCommunicator() (before and after)
|
|
```
|
|
|
|
---
|
|
|
|
## Per-Rank Logging
|
|
|
|
### Overview
|
|
|
|
By default, only **rank 0** output is displayed to the terminal to avoid cluttered logs from multiple processes. However, for debugging and detailed analysis, you can enable **per-rank logging** to capture output from all ranks into separate log files.
|
|
|
|
### Enabling Per-Rank Logging
|
|
|
|
Set the environment variable before running tests:
|
|
|
|
```bash
|
|
export RCCL_MPI_LOG_ALL_RANKS=1
|
|
mpirun -np 4 ./rccl-UnitTestsMPI --gtest_filter="MyTest.*"
|
|
```
|
|
|
|
Or in a single command:
|
|
|
|
```bash
|
|
RCCL_MPI_LOG_ALL_RANKS=1 mpirun -np 4 ./rccl-UnitTestsMPI
|
|
```
|
|
|
|
### Log Files
|
|
|
|
When enabled, log files are created for all ranks in the **current working directory**:
|
|
|
|
```
|
|
rccl_test_rank_0.log (contains rank 0 output - also displayed on console)
|
|
rccl_test_rank_1.log (contains all rank 1 output)
|
|
rccl_test_rank_2.log (contains all rank 2 output)
|
|
rccl_test_rank_3.log (contains all rank 3 output)
|
|
```
|
|
|
|
**Important Notes:**
|
|
- **Rank 0**: Output goes to BOTH console (for interactive monitoring) AND log file (for later analysis)
|
|
- **Rank 1-N**: Output goes only to log files
|
|
- **Location**: Log files are created in the directory where you execute the test command
|
|
- For multi-node runs, each node creates logs in its local working directory
|
|
- Ensure you have write permissions in the execution directory
|
|
|
|
**Banner Display:**
|
|
The per-rank logging banner is displayed using `TEST_INFO` macros, which means:
|
|
- **With `NCCL_DEBUG=INFO`**: Full banner with details shown
|
|
- **Without `NCCL_DEBUG`**: Banner content not shown (minimal output)
|
|
- This allows clean output in production while providing detailed info during debugging
|
|
|
|
### What Gets Logged
|
|
|
|
**For non-zero ranks (Ranks 1-N):**
|
|
All output is redirected to log files, including:
|
|
- Test progress and results (Google Test output)
|
|
- `printf()` and `std::cout` statements
|
|
- Error messages from NCCLCHECK, HIPCHECK, MPICHECK macros
|
|
- Debug output from RCCL internals
|
|
- Warnings and error messages
|
|
|
|
**For rank 0:**
|
|
- Output goes to BOTH console and log file (`rccl_test_rank_0.log`)
|
|
- **Console**: For real-time interactive monitoring
|
|
- **Log file**: For post-run analysis and comparison with other ranks
|
|
- Best of both worlds: watch progress live AND have complete logs for debugging
|
|
|
|
### Implementation Details
|
|
|
|
**How It Works:**
|
|
1. At startup, the framework checks `RCCL_MPI_LOG_ALL_RANKS` environment variable
|
|
2. If set to `1`, creates `rccl_test_rank_<N>.log` for each rank
|
|
3. Displays a banner using `TEST_INFO` macros (visible with `NCCL_DEBUG=INFO`)
|
|
4. **For Rank 0**: Uses "tee" functionality via pipe and background thread
|
|
- Creates a pipe
|
|
- Redirects stdout/stderr to pipe write end
|
|
- Background thread reads from pipe and writes to BOTH original console and log file
|
|
5. **For Ranks 1-N**: Redirects stdout/stderr directly to log file
|
|
6. Disables buffering for immediate output
|
|
7. Restores original output streams on exit
|
|
|
|
**Output Behavior:**
|
|
- **Without per-rank logging** (default): Only rank 0 output shown on terminal
|
|
- **With per-rank logging**:
|
|
- **Rank 0**: Output goes to BOTH console AND `rccl_test_rank_0.log`
|
|
- Watch test progress in real-time on console
|
|
- Complete log saved to file for later analysis
|
|
- **Ranks 1-N**: Output redirected to `rccl_test_rank_<N>.log` files only
|
|
- Banner visible when `NCCL_DEBUG=INFO` is set (TEST_INFO macros)
|
|
- Without `NCCL_DEBUG`, logging works silently with no banner clutter
|
|
|
|
### Usage Examples
|
|
|
|
#### Example 1: Debugging a Specific Test
|
|
|
|
```bash
|
|
# Enable per-rank logging with NCCL_DEBUG for full banner
|
|
export RCCL_MPI_LOG_ALL_RANKS=1
|
|
NCCL_DEBUG=INFO mpirun -np 2 ./rccl-UnitTestsMPI --gtest_filter="P2PTest.DataTransfer"
|
|
|
|
# You'll see a banner message at startup (with NCCL_DEBUG=INFO):
|
|
# [0] TEST INFO Per-Rank Logging ENABLED (RCCL_MPI_LOG_ALL_RANKS=1)
|
|
# [0] TEST INFO Rank 0 : Output to BOTH console AND rccl_test_rank_0.log
|
|
# [0] TEST INFO Ranks 1-N : Output redirected to rccl_test_rank_<N>.log
|
|
# [0] TEST INFO Location : Log files created in current working directory
|
|
|
|
# Without NCCL_DEBUG (minimal output):
|
|
export RCCL_MPI_LOG_ALL_RANKS=1
|
|
mpirun -np 2 ./rccl-UnitTestsMPI --gtest_filter="P2PTest.DataTransfer"
|
|
# (banner content not shown, but logging still enabled)
|
|
```
|
|
|
|
### Best Practices
|
|
|
|
**When to Enable:**
|
|
- ✅ Debugging test failures on non-zero ranks
|
|
- ✅ Investigating rank-specific behavior differences
|
|
- ✅ Analyzing communication patterns across ranks
|
|
- ✅ Capturing detailed RCCL internal logs from all processes
|
|
- ✅ Troubleshooting deadlocks or hangs
|
|
|
|
**When to Disable:**
|
|
- ✅ Normal test runs (cleaner output)
|
|
- ✅ CI/CD pipelines (unless debugging)
|
|
- ✅ Large-scale runs (many ranks generate many files)
|
|
|
|
### Adding Debug Output in Tests
|
|
|
|
Use `TEST_*` macros for conditional, rank-aware logging that respects `NCCL_DEBUG`:
|
|
|
|
```cpp
|
|
TEST_F(MyMPITest, DebugExample) {
|
|
validateTestPrerequisites(2);
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
// TEST_INFO respects NCCL_DEBUG=INFO setting
|
|
// Automatically includes rank (and hostname for multi-node)
|
|
TEST_INFO("Starting test with buffer size %zu", buffer_size);
|
|
|
|
// Perform operation
|
|
NCCLCHECK(ncclAllReduce(...));
|
|
|
|
// More debug output with automatic rank prefix
|
|
TEST_INFO("AllReduce completed, result=%f", result);
|
|
|
|
// Only rank 0 prints summary
|
|
if (MPIEnvironment::world_rank == 0) {
|
|
TEST_INFO("Summary: All ranks completed successfully");
|
|
}
|
|
}
|
|
```
|
|
|
|
**Available TEST_* Macros:**
|
|
```cpp
|
|
TEST_WARN("Warning message"); // NCCL_DEBUG=WARN or higher
|
|
TEST_INFO("Info message"); // NCCL_DEBUG=INFO or higher
|
|
TEST_ABORT("Abort message"); // NCCL_DEBUG=ABORT or higher
|
|
TEST_TRACE("Trace message"); // NCCL_DEBUG=TRACE
|
|
```
|
|
|
|
**Output Format:**
|
|
- Single-node: `[rank] TEST INFO <message>`
|
|
- Multi-node: `hostname:[rank] TEST INFO <message>`
|
|
|
|
**Example Output:**
|
|
```bash
|
|
# Single-node with NCCL_DEBUG=INFO
|
|
[0] TEST INFO Starting test with buffer size 1024
|
|
[1] TEST INFO Starting test with buffer size 1024
|
|
|
|
# Multi-node with NCCL_DEBUG=INFO
|
|
mi300x-3:[0] TEST INFO Starting test with buffer size 1024
|
|
mi300x-4:[1] TEST INFO Starting test with buffer size 1024
|
|
```
|
|
|
|
---
|
|
|
|
## API Reference
|
|
|
|
### MPITestBase Methods
|
|
|
|
#### `validateTestPrerequisites()`
|
|
Validate that the test has sufficient processes and correct node configuration to run.
|
|
|
|
```cpp
|
|
bool validateTestPrerequisites(
|
|
int min_processes = 1,
|
|
int max_processes = kNoProcessLimit,
|
|
bool require_power_of_two = false,
|
|
int min_nodes = 1,
|
|
int max_nodes = kNoNodeLimit
|
|
);
|
|
```
|
|
|
|
**Parameters:**
|
|
- `min_processes` - Minimum number of MPI processes required (default: 1)
|
|
- `max_processes` - Maximum number of MPI processes allowed (default: 0 = no limit)
|
|
- `require_power_of_two` - If true, process count must be power of 2 (default: false)
|
|
- `min_nodes` - Minimum number of nodes required (default: 1)
|
|
- `max_nodes` - Maximum number of nodes allowed (default: 0 = no limit)
|
|
|
|
**Returns:**
|
|
- `true` if all requirements are met
|
|
- `false` if requirements not met (test should skip)
|
|
|
|
**Behavior:**
|
|
- Displays detailed requirements and current environment on rank 0 (when `NCCL_DEBUG=INFO`)
|
|
- **Always performs node detection and displays process distribution** for visibility into actual MPI configuration
|
|
- If requirements not met: Returns false, typically used with `GTEST_SKIP()`
|
|
- Provides clear error messages explaining what's wrong
|
|
- Safe to call multiple times (though node detection runs each time)
|
|
- Node count is always detected and displayed regardless of whether node constraints are specified
|
|
|
|
**Process Validation:**
|
|
- `min_processes`: Minimum processes needed (test skips if world_size < min)
|
|
- `max_processes = 0` (default): No upper limit on process count
|
|
- `max_processes = N`: Test requires at most N processes (e.g., 2 = exactly 2 if min=2)
|
|
- When min_processes == max_processes: Test requires exactly that many processes
|
|
|
|
**Node Validation:**
|
|
- `min_nodes = 1` (default): No minimum node constraint
|
|
- `min_nodes = N`: Test requires at least N nodes
|
|
- `max_nodes = 0` (default): No node limit - test works on any number of nodes
|
|
- `max_nodes = N`: Test requires at most N nodes (e.g., 1 = single-node only)
|
|
- When min_nodes == max_nodes: Test requires exactly that many nodes
|
|
|
|
**Common Use Cases:**
|
|
- **Flexible process count**: Use only min_processes, let max default to 0
|
|
- **Exact process count**: Set min_processes = max_processes
|
|
- **Single-node features**: Set max_nodes = 1 (P2P, SHM, shared memory)
|
|
- **Multi-node required**: Set min_nodes > 1 (NET transport testing)
|
|
- **Power-of-two algorithms**: Set require_power_of_two = true
|
|
|
|
**Examples:**
|
|
```cpp
|
|
// Need at least 2 processes (any node configuration)
|
|
validateTestPrerequisites(2);
|
|
|
|
// Exactly 4 processes
|
|
validateTestPrerequisites(4, 4);
|
|
|
|
// At least 4 processes AND must be power of 2
|
|
validateTestPrerequisites(4, kNoProcessLimit, kRequirePowerOfTwo);
|
|
|
|
// Single-node only feature (P2P, SHM, or any intra-node algorithm)
|
|
validateTestPrerequisites(2, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kRequireSingleNode);
|
|
|
|
// Requires at least 2 nodes (multi-node testing)
|
|
validateTestPrerequisites(4, kNoProcessLimit, kNoPowerOfTwoRequired, 2);
|
|
|
|
// Exactly 8 processes, power-of-two, single-node only
|
|
validateTestPrerequisites(8, 8, kRequirePowerOfTwo, 1, kRequireSingleNode);
|
|
|
|
// 4-16 processes, must be on exactly 2 nodes
|
|
validateTestPrerequisites(4, 16, kNoPowerOfTwoRequired, 2, 2);
|
|
```
|
|
|
|
#### `createTestCommunicator()`
|
|
Create a test-specific RCCL communicator.
|
|
|
|
```cpp
|
|
ncclResult_t createTestCommunicator();
|
|
```
|
|
|
|
**Returns:** `ncclSuccess` on success, error code otherwise
|
|
|
|
**What it does:**
|
|
1. Rank 0 generates unique ID via `ncclGetUniqueId()`
|
|
2. Broadcast ID to all ranks via `MPI_Bcast()`
|
|
3. MPI barrier for synchronization
|
|
4. Initialize RCCL communicator with `ncclCommInitRank()`
|
|
5. Create HIP stream
|
|
6. MPI barrier after initialization
|
|
|
|
**Example:**
|
|
```cpp
|
|
TEST_F(MyTest, Example) {
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
ncclComm_t comm = getActiveCommunicator();
|
|
// Use comm for RCCL operations
|
|
}
|
|
```
|
|
|
|
#### `getActiveCommunicator()`
|
|
Get the current test communicator.
|
|
|
|
```cpp
|
|
ncclComm_t getActiveCommunicator();
|
|
```
|
|
|
|
**Returns:** The test-specific communicator, or `nullptr` with test failure
|
|
|
|
**Important:** Must call `createTestCommunicator()` first!
|
|
|
|
#### `getActiveStream()`
|
|
Get the current HIP stream.
|
|
|
|
```cpp
|
|
hipStream_t getActiveStream();
|
|
```
|
|
|
|
**Returns:** The test-specific stream, or `nullptr` with test failure
|
|
|
|
**Important:** Must call `createTestCommunicator()` first!
|
|
|
|
#### `cleanupTestCommunicator()`
|
|
Clean up test-specific resources.
|
|
|
|
```cpp
|
|
void cleanupTestCommunicator();
|
|
```
|
|
|
|
**What it does:**
|
|
1. MPI barrier before cleanup
|
|
2. Destroy HIP stream
|
|
3. Destroy RCCL communicator
|
|
4. MPI barrier after cleanup
|
|
|
|
**Note:** Automatically called in `TearDown()` - usually don't need to call manually.
|
|
|
|
### MPIEnvironment Static Members
|
|
|
|
```cpp
|
|
// Current process rank (0 to world_size-1)
|
|
int MPIEnvironment::world_rank;
|
|
|
|
// Total number of processes
|
|
int MPIEnvironment::world_size;
|
|
|
|
// Initialization return code (0 = success)
|
|
int MPIEnvironment::retCode;
|
|
```
|
|
|
|
### MPITestConstants
|
|
|
|
```cpp
|
|
namespace MPITestConstants {
|
|
// Minimum processes for MPI tests
|
|
constexpr int kMinProcessesForMPI = 2;
|
|
|
|
// Flags for process count validation
|
|
constexpr bool kRequirePowerOfTwo = true;
|
|
constexpr bool kNoPowerOfTwoRequired = false;
|
|
constexpr int kNoProcessLimit = 0; // No upper limit on process count
|
|
|
|
// Flags for node count validation
|
|
constexpr int kRequireSingleNode = 1; // For single-node only tests
|
|
constexpr int kNoNodeLimit = 0; // For multi-node capable tests (default)
|
|
|
|
// Helper functions
|
|
bool isPowerOfTwo(int n);
|
|
int detectNodeCount(); // Detects unique nodes via MPI shared memory domains
|
|
}
|
|
```
|
|
|
|
**Node Detection:**
|
|
The `detectNodeCount()` function automatically detects the number of unique physical nodes by:
|
|
1. Splitting MPI communicator by shared memory domain using `MPI_Comm_split_type()` with `MPI_COMM_TYPE_SHARED`
|
|
2. Each rank determines its local rank and node size (ranks per node)
|
|
3. All node sizes are gathered to rank 0
|
|
4. Rank 0 analyzes the distribution to count unique nodes
|
|
5. Node count is broadcast to all ranks
|
|
6. Returns number of unique nodes
|
|
|
|
This uses standard MPI-3 functionality to detect physical node boundaries based on shared memory accessibility, working automatically with any MPI implementation and job scheduler.
|
|
|
|
**Usage Example:**
|
|
```cpp
|
|
// Automatically called by validateTestPrerequisites()
|
|
int nodes = MPITestConstants::detectNodeCount();
|
|
printf("Detected %d unique node(s)\n", nodes);
|
|
```
|
|
|
|
### Helper Macros
|
|
|
|
```cpp
|
|
// MPI error checking
|
|
ASSERT_MPI_SUCCESS(MPI_Function()); // GTest assertion-based
|
|
|
|
// RCCL error checking
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclFunction()); // GTest FAIL on error
|
|
|
|
// HIP error checking
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipFunction()); // GTest FAIL on error
|
|
```
|
|
|
|
---
|
|
|
|
## Examples
|
|
|
|
### Example 1: Basic AllReduce Test
|
|
|
|
```cpp
|
|
TEST_F(UnifiedMPITest, BasicAllReduce) {
|
|
// Validate we have at least 2 processes (uses defaults for other parameters)
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
const int N = 1024;
|
|
float *d_send = nullptr, *d_recv = nullptr;
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&d_send, N * sizeof(float)));
|
|
auto send_guard = makeDeviceBufferAutoGuard(d_send);
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&d_recv, N * sizeof(float)));
|
|
auto recv_guard = makeDeviceBufferAutoGuard(d_recv);
|
|
|
|
// Initialize with rank-based pattern using DeviceBufferHelpers
|
|
hipError_t hip_result = initializeBufferWithPattern<float>(
|
|
d_send, N,
|
|
[rank = MPIEnvironment::world_rank](size_t i) {
|
|
return static_cast<float>(rank + 1);
|
|
});
|
|
ASSERT_EQ(hipSuccess, hip_result);
|
|
|
|
// AllReduce: Sum across all ranks
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclAllReduce(d_send, d_recv, N, ncclFloat, ncclSum,
|
|
getActiveCommunicator(), getActiveStream()));
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(getActiveStream()));
|
|
|
|
// Verify using DeviceBufferHelpers with pattern function
|
|
// Expected: sum of (1 + 2 + 3 + ... + world_size)
|
|
const float expected_sum = (MPIEnvironment::world_size *
|
|
(MPIEnvironment::world_size + 1)) / 2.0f;
|
|
|
|
size_t error_idx;
|
|
float expected_val, actual_val;
|
|
bool data_correct = verifyBufferData<float>(
|
|
d_recv, N,
|
|
[expected_sum](size_t i) { return expected_sum; }, // All elements same
|
|
0, // Verify all elements
|
|
1e-5,
|
|
&error_idx, &expected_val, &actual_val);
|
|
|
|
EXPECT_TRUE(data_correct) << "Data mismatch at index " << error_idx
|
|
<< ": expected " << expected_val
|
|
<< ", got " << actual_val;
|
|
|
|
// Automatic cleanup via RAII guards
|
|
}
|
|
```
|
|
|
|
### Example 2: Broadcast Test
|
|
|
|
```cpp
|
|
TEST_F(UnifiedMPITest, Broadcast) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
const int N = 1000;
|
|
float *d_data = nullptr;
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&d_data, N * sizeof(float)));
|
|
auto data_guard = makeDeviceBufferAutoGuard(d_data);
|
|
|
|
// Initialize using DeviceBufferHelpers - rank 0 gets sequential, others get zeros
|
|
if (MPIEnvironment::world_rank == 0) {
|
|
hipError_t hip_result = initializeBufferWithPattern<float>(
|
|
d_data, N,
|
|
[](size_t i) { return static_cast<float>(i + 1); }); // 1, 2, 3, ...
|
|
ASSERT_EQ(hipSuccess, hip_result);
|
|
} else {
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMemset(d_data, 0, N * sizeof(float)));
|
|
}
|
|
|
|
// Broadcast from rank 0 to all ranks
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclBroadcast(d_data, d_data, N, ncclFloat, 0,
|
|
getActiveCommunicator(), getActiveStream()));
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(getActiveStream()));
|
|
|
|
// Verify all ranks have the broadcast data using DeviceBufferHelpers
|
|
size_t error_idx;
|
|
float expected_val, actual_val;
|
|
bool data_correct = verifyBufferData<float>(
|
|
d_data, N,
|
|
[](size_t i) { return static_cast<float>(i + 1); }, // Expected: 1, 2, 3, ...
|
|
0, // Verify all elements
|
|
1e-5,
|
|
&error_idx, &expected_val, &actual_val);
|
|
|
|
EXPECT_TRUE(data_correct) << "Rank " << MPIEnvironment::world_rank
|
|
<< ": Data mismatch at index " << error_idx
|
|
<< ": expected " << expected_val
|
|
<< ", got " << actual_val;
|
|
|
|
// Automatic cleanup via RAII guards
|
|
}
|
|
```
|
|
|
|
### Example 3: Send/Recv Between Ranks
|
|
|
|
```cpp
|
|
TEST_F(UnifiedMPITest, SimpleSendRecv) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
const int N = 1024;
|
|
const int peer_rank = 1 - MPIEnvironment::world_rank; // 0↔1
|
|
|
|
float* d_send = nullptr;
|
|
float* d_recv = nullptr;
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&d_send, N * sizeof(float)));
|
|
auto send_guard = makeDeviceBufferAutoGuard(d_send);
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&d_recv, N * sizeof(float)));
|
|
auto recv_guard = makeDeviceBufferAutoGuard(d_recv);
|
|
|
|
// Initialize send buffer with rank-specific pattern using DeviceBufferHelpers
|
|
hipError_t hip_result = initializeBufferWithPattern<float>(
|
|
d_send, N,
|
|
[rank = MPIEnvironment::world_rank](size_t i) {
|
|
return static_cast<float>(rank * 1000 + i);
|
|
});
|
|
ASSERT_EQ(hipSuccess, hip_result);
|
|
|
|
// Exchange data between ranks
|
|
if (MPIEnvironment::world_rank == 0) {
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclSend(d_send, N, ncclFloat, 1,
|
|
getActiveCommunicator(), getActiveStream()));
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclRecv(d_recv, N, ncclFloat, 1,
|
|
getActiveCommunicator(), getActiveStream()));
|
|
} else {
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclRecv(d_recv, N, ncclFloat, 0,
|
|
getActiveCommunicator(), getActiveStream()));
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclSend(d_send, N, ncclFloat, 0,
|
|
getActiveCommunicator(), getActiveStream()));
|
|
}
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(getActiveStream()));
|
|
|
|
// Verify received peer's data using DeviceBufferHelpers
|
|
size_t error_idx;
|
|
float expected_val, actual_val;
|
|
bool data_correct = verifyBufferData<float>(
|
|
d_recv, N,
|
|
[peer_rank](size_t i) {
|
|
return static_cast<float>(peer_rank * 1000 + i);
|
|
},
|
|
0, // Verify all elements
|
|
1e-5,
|
|
&error_idx, &expected_val, &actual_val);
|
|
|
|
EXPECT_TRUE(data_correct) << "Rank " << MPIEnvironment::world_rank
|
|
<< ": Received data mismatch at index " << error_idx
|
|
<< ": expected " << expected_val
|
|
<< ", got " << actual_val;
|
|
|
|
// Automatic cleanup via RAII guards
|
|
}
|
|
```
|
|
|
|
### Example 4: Testing Different Reduction Operations
|
|
|
|
```cpp
|
|
TEST_F(UnifiedMPITest, AllReduceMaxOperation) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
const int N = 512;
|
|
float* d_send = nullptr;
|
|
float* d_recv = nullptr;
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&d_send, N * sizeof(float)));
|
|
auto send_guard = makeDeviceBufferAutoGuard(d_send);
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&d_recv, N * sizeof(float)));
|
|
auto recv_guard = makeDeviceBufferAutoGuard(d_recv);
|
|
|
|
// Initialize with rank-specific pattern using DeviceBufferHelpers
|
|
hipError_t hip_result = initializeBufferWithPattern<float>(
|
|
d_send, N,
|
|
[rank = MPIEnvironment::world_rank](size_t i) {
|
|
return static_cast<float>(rank * 10 + i);
|
|
});
|
|
ASSERT_EQ(hipSuccess, hip_result);
|
|
|
|
// AllReduce with MAX operation
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclAllReduce(d_send, d_recv, N, ncclFloat, ncclMax,
|
|
getActiveCommunicator(), getActiveStream()));
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(getActiveStream()));
|
|
|
|
// Verify: maximum should be from highest rank using DeviceBufferHelpers
|
|
const int max_rank = MPIEnvironment::world_size - 1;
|
|
|
|
size_t error_idx;
|
|
float expected_val, actual_val;
|
|
bool data_correct = verifyBufferData<float>(
|
|
d_recv, N,
|
|
[max_rank](size_t i) {
|
|
return static_cast<float>(max_rank * 10 + i);
|
|
},
|
|
0, // Verify all elements
|
|
1e-5,
|
|
&error_idx, &expected_val, &actual_val);
|
|
|
|
EXPECT_TRUE(data_correct) << "AllReduce MAX mismatch at index " << error_idx
|
|
<< ": expected " << expected_val
|
|
<< ", got " << actual_val;
|
|
|
|
// Automatic cleanup via RAII guards
|
|
}
|
|
```
|
|
|
|
### Example 5: Power-of-Two Requirement
|
|
|
|
```cpp
|
|
TEST_F(MyMPITest, AdvancedAlgorithm) {
|
|
// This algorithm requires power-of-two processes (at least 4)
|
|
ASSERT_TRUE(validateTestPrerequisites(4, kNoProcessLimit, kRequirePowerOfTwo));
|
|
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
// Test only runs with 4, 8, 16, 32, ... processes
|
|
// Automatically skipped if run with 3, 5, 6, 7, ... processes
|
|
|
|
// Your test logic here
|
|
}
|
|
```
|
|
|
|
### Example 6: Single-Node Only Test
|
|
|
|
Tests that require all processes on the same physical node should use `kRequireSingleNode`:
|
|
|
|
```cpp
|
|
TEST_F(P2pMPITest, P2pWorkflow) {
|
|
// This test requires single-node execution
|
|
// Common reasons: direct GPU access, shared memory, local IPC, hardware topology
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kRequireSingleNode));
|
|
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
// Test runs on single node:
|
|
// Test skips on multi-node with informative message:
|
|
// "Error: REQUIREMENT NOT MET: Need at most 1 node(s), detected 2 nodes"
|
|
// "This test uses P2P/SHM transport (single-node only)"
|
|
// "For multi-node testing, use NET transport tests"
|
|
|
|
// Your single-node test logic here...
|
|
// Examples: P2P transport, SHM transport, GPU topology tests,
|
|
// shared memory algorithms, local IPC features
|
|
}
|
|
```
|
|
|
|
### Example 7: Multi-Node Capable Test
|
|
|
|
Tests that work across multiple nodes should use `kNoNodeLimit` (or omit the parameter):
|
|
|
|
```cpp
|
|
TEST_F(NetMPITest, NetWorkflow) {
|
|
// This test works on any number of nodes
|
|
// Common reasons: network-based, distributed features, scalability tests
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
// Uses defaults: no max processes, not power-of-two, any nodes
|
|
// (kNoNodeLimit is the default for max_nodes)
|
|
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
// Test runs on single node:
|
|
// Test runs on multi-node:
|
|
// Works with any node configuration
|
|
|
|
// Your multi-node test logic here...
|
|
// Examples: NET transport, distributed collectives, RDMA features,
|
|
// scalability tests, network-based algorithms
|
|
}
|
|
```
|
|
|
|
### Example 8: Custom Test Class with RAII Resource Guards
|
|
|
|
```cpp
|
|
class MyTransportTest : public TransportTestBase {
|
|
protected:
|
|
void* send_buffer = nullptr;
|
|
void* recv_buffer = nullptr;
|
|
size_t buffer_size = 1024 * 1024; // 1MB
|
|
|
|
void SetUp() override {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
// Allocate buffers with automatic RAII guards
|
|
// Guards stored in base class, cleanup automatic at test end
|
|
allocateAndInitBuffersGuarded(&send_buffer, &recv_buffer, buffer_size, buffer_size);
|
|
}
|
|
|
|
// No need for manual cleanup in TearDown()
|
|
// Base class automatically cleans up guarded resources
|
|
};
|
|
|
|
TEST_F(MyTransportTest, DataTransfer) {
|
|
// Initialize buffers using DeviceBufferHelpers
|
|
hipError_t hip_result = initializeBufferWithPattern<uint8_t>(
|
|
send_buffer, buffer_size,
|
|
[](size_t i) { return static_cast<uint8_t>(0xAB); });
|
|
ASSERT_EQ(hipSuccess, hip_result);
|
|
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMemset(recv_buffer, 0x00, buffer_size));
|
|
|
|
// Perform transfer
|
|
// ... test logic ...
|
|
|
|
// Verify using DeviceBufferHelpers
|
|
size_t error_idx;
|
|
uint8_t expected_val, actual_val;
|
|
bool data_correct = verifyBufferData<uint8_t>(
|
|
recv_buffer, buffer_size,
|
|
[](size_t i) { return static_cast<uint8_t>(0xAB); },
|
|
0, // Verify all elements
|
|
1e-5,
|
|
&error_idx, &expected_val, &actual_val);
|
|
|
|
EXPECT_TRUE(data_correct) << "Data transfer mismatch at index " << error_idx
|
|
<< ": expected " << static_cast<int>(expected_val)
|
|
<< ", got " << static_cast<int>(actual_val);
|
|
|
|
// Resources automatically cleaned up at test end
|
|
}
|
|
```
|
|
|
|
### Example 9: Loop with Per-Iteration Cleanup
|
|
|
|
For tests that allocate resources in loops, use `store_in_base=false` to get local guards:
|
|
|
|
```cpp
|
|
TEST_F(MyTransportTest, TestMultipleSizes) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
const std::vector<size_t> test_sizes = {1024, 4096, 16384, 65536};
|
|
|
|
for(const auto size : test_sizes) {
|
|
void* send_buff = nullptr;
|
|
void* recv_buff = nullptr;
|
|
|
|
// Get local guards - cleanup at end of each iteration
|
|
auto [sendGuard, recvGuard] = allocateAndInitBuffersGuarded(
|
|
&send_buff, &recv_buff, size, size, false); // false = local guards
|
|
|
|
// Test with this buffer size
|
|
testTransfer(send_buff, recv_buff, size);
|
|
|
|
// Buffers automatically freed here at end of iteration
|
|
}
|
|
|
|
// All buffers already cleaned up, minimal memory footprint maintained
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
### 1. Use RAII Guards for Automatic Resource Cleanup
|
|
|
|
**TransportTestBase** provides RAII-based resource management to prevent leaks:
|
|
|
|
```cpp
|
|
// ✅ GOOD: Use guarded allocation (default - cleanup at test end)
|
|
TEST_F(MyTransportTest, Example) {
|
|
void* send_buffer = nullptr;
|
|
void* recv_buffer = nullptr;
|
|
|
|
// Allocate with automatic guards
|
|
allocateAndInitBuffersGuarded(&send_buffer, &recv_buffer, size, size);
|
|
|
|
// Use buffers...
|
|
|
|
// Automatic cleanup even if assertions fail!
|
|
}
|
|
|
|
// ✅ GOOD: Loop with per-iteration cleanup
|
|
TEST_F(MyTransportTest, LoopExample) {
|
|
for(const auto size : test_sizes) {
|
|
void* send_buff = nullptr;
|
|
void* recv_buff = nullptr;
|
|
|
|
// Local guards - cleanup per iteration
|
|
auto [sg, rg] = allocateAndInitBuffersGuarded(&send_buff, &recv_buff, size, size, false);
|
|
|
|
// Test logic...
|
|
// Cleanup happens here automatically
|
|
}
|
|
}
|
|
|
|
// ❌ BAD: Manual cleanup can leak on assertion failure
|
|
TEST_F(MyTransportTest, Example) {
|
|
void* send_buffer = nullptr;
|
|
hipMalloc(&send_buffer, size);
|
|
|
|
ASSERT_TRUE(condition); // If this fails, send_buffer leaks!
|
|
|
|
hipFree(send_buffer); // Never reached if assertion fails
|
|
}
|
|
```
|
|
|
|
**RAII Guard Benefits:**
|
|
- ✅ Resources cleaned up even if `ASSERT_*` or `EXPECT_*` fails
|
|
- ✅ Exception-safe cleanup
|
|
- ✅ No manual cleanup code needed
|
|
- ✅ Prevents memory leaks in test failures
|
|
- ✅ Supports both test-scoped and loop-scoped cleanup
|
|
|
|
**API:**
|
|
```cpp
|
|
// Store guards in base class (cleanup at test end) - DEFAULT
|
|
allocateAndInitBuffersGuarded(&send, &recv, size, size);
|
|
|
|
// Get local guards (cleanup at scope exit) - FOR LOOPS
|
|
auto [sendGuard, recvGuard] = allocateAndInitBuffersGuarded(&send, &recv, size, size, false);
|
|
|
|
// Registration with guards
|
|
preRegisterBuffersGuarded(send, recv, size, size, &send_handle, &recv_handle);
|
|
auto [sendRegGuard, recvRegGuard] = preRegisterBuffersGuarded(..., false);
|
|
```
|
|
|
|
### 2. Always Validate Prerequisites
|
|
|
|
```cpp
|
|
TEST_F(MyTest, SomeTest) {
|
|
// ✅ GOOD: Validate first
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
// Test logic...
|
|
}
|
|
|
|
// ❌ BAD: No validation
|
|
TEST_F(MyTest, SomeTest) {
|
|
// Might crash if only 1 process!
|
|
ncclAllReduce(...);
|
|
}
|
|
```
|
|
|
|
### 3. Create Test-Specific Communicators
|
|
|
|
```cpp
|
|
// ✅ GOOD: Isolated communicator per test
|
|
TEST_F(MyTest, Test1) {
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
// Test logic with fresh communicator
|
|
}
|
|
|
|
TEST_F(MyTest, Test2) {
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
// Another test with its own communicator
|
|
}
|
|
```
|
|
|
|
**Why?** Avoids shared memory conflicts and ensures clean state.
|
|
|
|
### 4. Use RAII Guards for Resource Management
|
|
|
|
**TransportTestBase** provides RAII-based automatic resource cleanup with proper ordering:
|
|
|
|
```cpp
|
|
// ✅ GOOD: Automatic cleanup even on test failure
|
|
TEST_F(MyTransportTest, Example) {
|
|
void* send_buffer = nullptr;
|
|
void* recv_buffer = nullptr;
|
|
|
|
// Allocate with automatic guards (default: cleanup at test end)
|
|
allocateAndInitBuffersGuarded(&send_buffer, &recv_buffer, size, size);
|
|
|
|
ASSERT_TRUE(condition); // Even if this fails, buffers cleaned up!
|
|
|
|
// Use buffers...
|
|
// Automatic cleanup at test end
|
|
}
|
|
|
|
// ✅ GOOD: Registration handles with guards
|
|
TEST_F(MyTransportTest, RegistrationExample) {
|
|
void* send_buffer = nullptr;
|
|
void* recv_buffer = nullptr;
|
|
allocateAndInitBuffersGuarded(&send_buffer, &recv_buffer, size, size);
|
|
|
|
// Pre-register with guards - handles deregistered before comm destroyed
|
|
void* send_handle = nullptr;
|
|
void* recv_handle = nullptr;
|
|
preRegisterBuffersGuarded(send_buffer, recv_buffer, size, size,
|
|
&send_handle, &recv_handle);
|
|
|
|
// Use registered buffers...
|
|
// Cleanup order: handles deregistered → buffers freed → comm destroyed
|
|
}
|
|
|
|
// ✅ GOOD: Loop with per-iteration cleanup
|
|
TEST_F(MyTransportTest, LoopTest) {
|
|
for(const auto size : test_sizes) {
|
|
void* send_buff = nullptr;
|
|
void* recv_buff = nullptr;
|
|
|
|
// Get local guards (store_in_base=false for per-iteration cleanup)
|
|
auto [sendGuard, recvGuard] = allocateAndInitBuffersGuarded(
|
|
&send_buff, &recv_buff, size, size, false);
|
|
|
|
// Test logic...
|
|
// Cleanup happens here automatically at end of iteration
|
|
}
|
|
}
|
|
|
|
// ❌ BAD: Manual cleanup leaks on assertion failure
|
|
TEST_F(MyTest, Example) {
|
|
void* buffer = nullptr;
|
|
hipMalloc(&buffer, size);
|
|
|
|
ASSERT_TRUE(condition); // If fails, buffer leaks!
|
|
|
|
hipFree(buffer); // Never reached
|
|
}
|
|
```
|
|
|
|
**RAII Guard API:**
|
|
```cpp
|
|
// Allocate with guards (cleanup at test end) - DEFAULT
|
|
allocateAndInitBuffersGuarded(&send, &recv, size, size);
|
|
|
|
// Allocate with local guards (cleanup at scope exit) - FOR LOOPS
|
|
auto [sendGuard, recvGuard] = allocateAndInitBuffersGuarded(&send, &recv, size, size, false);
|
|
|
|
// Register buffers with guards (cleanup at test end) - DEFAULT
|
|
preRegisterBuffersGuarded(send, recv, size, size, &send_handle, &recv_handle);
|
|
|
|
// Register with local guards (for loops)
|
|
auto [sRegGuard, rRegGuard] = preRegisterBuffersGuarded(send, recv, size, size,
|
|
&send_handle, &recv_handle, false);
|
|
```
|
|
|
|
**Benefits:**
|
|
- ✅ Resources cleaned up even if `ASSERT_*` or `EXPECT_*` fails
|
|
- ✅ **Correct cleanup order**: Guards destroyed before communicator
|
|
- ✅ Exception-safe cleanup
|
|
- ✅ No manual cleanup code needed
|
|
- ✅ Prevents memory leaks in test failures
|
|
- ✅ Prevents "corrupted comm object" errors
|
|
- ✅ Supports both test-scoped and loop-scoped cleanup
|
|
|
|
**Critical: Cleanup Order**
|
|
The framework ensures proper cleanup order to prevent "corrupted comm object" errors:
|
|
```
|
|
1. Registration handles deregistered (ncclCommDeregister with valid comm)
|
|
2. Buffers freed (hipFree)
|
|
3. Transport resources cleaned up
|
|
4. Communicator destroyed (ncclCommDestroy)
|
|
```
|
|
|
|
This is handled automatically by `TransportTestBase::TearDown()` which explicitly clears
|
|
guard vectors before destroying the communicator.
|
|
|
|
### 5. Use Rank-Specific Logic When Needed
|
|
|
|
```cpp
|
|
TEST_F(MyTest, Example) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
int rank = MPIEnvironment::world_rank;
|
|
|
|
if (rank == 0) {
|
|
// Only rank 0 prints summary (TEST_INFO automatically adds rank prefix)
|
|
TEST_INFO("Starting test with %d processes", MPIEnvironment::world_size);
|
|
}
|
|
|
|
// All ranks execute this
|
|
performCollectiveOperation();
|
|
|
|
if (rank == 0) {
|
|
// Only rank 0 does final verification
|
|
verifyGlobalState();
|
|
TEST_INFO("Test completed successfully");
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Use Descriptive Test Names
|
|
|
|
```cpp
|
|
// ✅ GOOD: Clear what's being tested
|
|
TEST_F(MyMPITest, AllReduce_WithFloat32_Sum_2Ranks)
|
|
TEST_F(MyMPITest, Broadcast_LargeBuffer_FromRank0)
|
|
TEST_F(MyMPITest, SendRecv_PeerToPeer_1MBTransfer)
|
|
|
|
// ❌ BAD: Vague names
|
|
TEST_F(MyMPITest, Test1)
|
|
TEST_F(MyMPITest, TestAllReduce)
|
|
```
|
|
|
|
### 7. Check Return Codes
|
|
|
|
```cpp
|
|
// ✅ GOOD: Check all return codes with appropriate macros
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclAllReduce(...));
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&ptr, size));
|
|
|
|
// BAD: Ignoring return values
|
|
createTestCommunicator(); // Might fail silently!
|
|
ncclAllReduce(...); // Could return error
|
|
hipMalloc(&ptr, size); // Allocation might fail
|
|
```
|
|
|
|
### 8. Synchronize Appropriately
|
|
|
|
```cpp
|
|
// ✅ GOOD: Synchronize before checking results
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclAllReduce(...));
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(getActiveStream()));
|
|
// Now safe to verify results
|
|
|
|
// ❌ BAD: Check results without sync
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclAllReduce(...));
|
|
EXPECT_EQ(result, expected); // Operation might not be done!
|
|
```
|
|
|
|
### 9. Consider Process Count in Expectations
|
|
|
|
```cpp
|
|
TEST_F(MyTest, AllReduceSum) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
// Each rank sends 1
|
|
// Expected result depends on world_size
|
|
float expected = MPIEnvironment::world_size * 1.0f;
|
|
|
|
// ✅ GOOD: Expectation adapts to process count
|
|
EXPECT_FLOAT_EQ(result, expected);
|
|
|
|
// ❌ BAD: Hard-coded expectation
|
|
// EXPECT_FLOAT_EQ(result, 2.0f); // Only works with 2 processes!
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## RAII Resource Guards
|
|
|
|
The test infrastructure provides comprehensive RAII (Resource Acquisition Is Initialization) guards for automatic resource cleanup. These guards ensure resources are cleaned up even when tests fail via `ASSERT_*` or `EXPECT_*` failures.
|
|
|
|
### Overview
|
|
|
|
Two types of guards are provided:
|
|
|
|
1. **`AutoGuard<T, auto DeleterFunc>`** - Modern C++17 guard using function pointers (for simple cleanup)
|
|
2. **`ResourceGuard<T, Deleter>`** - Functor-based guard (for complex stateful cleanup)
|
|
|
|
### AutoGuard - Simple Cleanup
|
|
|
|
For resources with simple, stateless cleanup functions:
|
|
|
|
```cpp
|
|
// Device memory
|
|
void* buffer;
|
|
hipMalloc(&buffer, size);
|
|
auto guard = makeDeviceBufferAutoGuard(buffer);
|
|
// buffer automatically freed on scope exit
|
|
|
|
// HIP stream
|
|
hipStream_t stream;
|
|
hipStreamCreate(&stream);
|
|
auto guard = makeStreamAutoGuard(stream);
|
|
// stream automatically destroyed on scope exit
|
|
|
|
// HIP event
|
|
hipEvent_t event;
|
|
hipEventCreate(&event);
|
|
auto guard = makeEventAutoGuard(event);
|
|
// event automatically destroyed on scope exit
|
|
|
|
// Host memory
|
|
void* host_buf = malloc(size);
|
|
auto guard = makeHostBufferAutoGuard(host_buf);
|
|
// host_buf automatically freed on scope exit
|
|
|
|
// NCCL communicator
|
|
ncclComm_t comm;
|
|
ncclCommInitRank(&comm, ...);
|
|
auto guard = makeCommAutoGuard(comm);
|
|
// comm automatically destroyed on scope exit
|
|
```
|
|
|
|
### ResourceGuard - Complex Cleanup
|
|
|
|
For resources requiring additional context (stateful deleters):
|
|
|
|
```cpp
|
|
// NCCL registration handle (needs communicator)
|
|
void* reg_handle;
|
|
ncclCommRegister(comm, buffer, size, ®_handle);
|
|
auto guard = makeRegHandleGuard(reg_handle, comm);
|
|
// reg_handle automatically deregistered on scope exit
|
|
|
|
// NET plugin memory handle (needs net plugin + comm)
|
|
void* mhandle;
|
|
net->regMr(comm, buffer, size, type, &mhandle);
|
|
auto guard = makeNetMHandleGuard(mhandle, net, comm);
|
|
// mhandle automatically deregistered on scope exit
|
|
|
|
// NET send communicator (needs net plugin)
|
|
void* send_comm;
|
|
net->connect(dev, handle, &send_comm, &send_dev_handle);
|
|
auto guard = makeNetSendCommGuard(send_comm, net);
|
|
// send_comm automatically closed on scope exit
|
|
```
|
|
|
|
### Guard Operations
|
|
|
|
All guards support these operations:
|
|
|
|
```cpp
|
|
auto guard = makeDeviceBufferAutoGuard(buffer);
|
|
|
|
// Get the resource handle
|
|
void* buf = guard.get();
|
|
|
|
// Get pointer to handle (for API calls that take T*)
|
|
void** buf_ptr = guard.ptr();
|
|
|
|
// Set a new resource
|
|
guard.set(new_buffer);
|
|
|
|
// Release ownership (prevent cleanup)
|
|
void* released = guard.release();
|
|
|
|
// Dismiss without returning (prevent cleanup)
|
|
guard.dismiss();
|
|
```
|
|
|
|
### Specialized Guards
|
|
|
|
#### DeviceBufferAutoGuard / HostBufferAutoGuard - Host or Device Memory
|
|
|
|
Manages both host and device memory with type-safe guards:
|
|
|
|
```cpp
|
|
void* device_buf;
|
|
hipMalloc(&device_buf, size);
|
|
auto dev_guard = makeDeviceBufferAutoGuard(device_buf); // device memory
|
|
|
|
void* host_buf = malloc(size);
|
|
auto host_guard = makeHostBufferAutoGuard(host_buf); // host memory
|
|
|
|
// Both automatically freed on scope exit with correct function
|
|
```
|
|
|
|
#### NetConnectionGuard - Multiple Network Resources
|
|
|
|
Manages listen, send, and recv communicators together:
|
|
|
|
```cpp
|
|
NetConnectionGuard conn_guard(net_plugin);
|
|
|
|
// Set resources as they're created
|
|
conn_guard.setListenComm(listen_comm);
|
|
conn_guard.setSendComm(send_comm);
|
|
conn_guard.setRecvComm(recv_comm);
|
|
|
|
// All automatically closed on scope exit in correct order
|
|
```
|
|
|
|
#### TransportResourceGuard - Send and Recv Together
|
|
|
|
Manages paired send/recv transport resources:
|
|
|
|
```cpp
|
|
ncclConnector send_conn, recv_conn;
|
|
TransportResourceGuard guard(&send_conn, &recv_conn, transport);
|
|
|
|
// Both connectors automatically cleaned up on scope exit
|
|
```
|
|
|
|
### Factory Methods
|
|
|
|
Prefer factory methods for type deduction and cleaner syntax:
|
|
|
|
```cpp
|
|
// ✅ GOOD: Factory method (type deduced)
|
|
auto guard = makeDeviceBufferAutoGuard(buffer);
|
|
|
|
// ❌ VERBOSE: Explicit type (harder to read)
|
|
AutoGuard<void*, hipFreeWrapper> guard(buffer);
|
|
|
|
// ✅ GOOD: Complex guard with factory
|
|
auto guard = makeRegHandleGuard(handle, comm);
|
|
|
|
// ❌ VERBOSE: Explicit type
|
|
ResourceGuard<void*, NcclRegHandleDeleter> guard(handle, NcclRegHandleDeleter(comm));
|
|
```
|
|
|
|
### Available Factory Methods
|
|
|
|
**Simple Resources (AutoGuard):**
|
|
- `makeHostBufferAutoGuard(void* buffer)` - Host memory
|
|
- `makeDeviceBufferAutoGuard(void* buffer)` - Device memory
|
|
- `makeStreamAutoGuard(hipStream_t stream)` - HIP stream
|
|
- `makeEventAutoGuard(hipEvent_t event)` - HIP event
|
|
- `makeCommAutoGuard(ncclComm_t comm)` - NCCL communicator
|
|
|
|
**Complex Resources (ResourceGuard):**
|
|
- `makeRegHandleGuard(void* handle, ncclComm_t comm)` - NCCL registration
|
|
- `makeNetMHandleGuard(void* handle, ncclNet_t* net, void* comm)` - NET memory
|
|
- `makeNetSendCommGuard(void* comm, ncclNet_t* net)` - NET send comm
|
|
- `makeNetRecvCommGuard(void* comm, ncclNet_t* net)` - NET recv comm
|
|
- `makeNetListenCommGuard(void* comm, ncclNet_t* net)` - NET listen comm
|
|
- `makeTransportSendGuard(ncclConnector* conn, ncclTransport* trans)` - Transport send
|
|
- `makeTransportRecvGuard(ncclConnector* conn, ncclTransport* trans)` - Transport recv
|
|
|
|
**Generic:**
|
|
- `makeGuard(T resource, Deleter deleter)` - Generic ResourceGuard
|
|
- `makeCustomGuard(T resource, Deleter deleter)` - Custom deleter (alias)
|
|
|
|
|
|
### Best Practices with Guards
|
|
|
|
**1. Use Guards for All Resources:**
|
|
```cpp
|
|
// ✅ GOOD: Guards ensure cleanup even on assertion failure
|
|
TEST_F(MyTest, Example) {
|
|
void* buffer;
|
|
hipMalloc(&buffer, size);
|
|
auto guard = makeDeviceBufferAutoGuard(buffer);
|
|
|
|
ASSERT_TRUE(condition); // If fails, buffer still freed!
|
|
|
|
// Use buffer...
|
|
// Automatic cleanup on scope exit
|
|
}
|
|
|
|
// ❌ BAD: Manual cleanup leaks on assertion failure
|
|
TEST_F(MyTest, Example) {
|
|
void* buffer;
|
|
hipMalloc(&buffer, size);
|
|
|
|
ASSERT_TRUE(condition); // If fails, buffer leaks!
|
|
|
|
hipFree(buffer); // Never reached
|
|
}
|
|
```
|
|
|
|
**2. Respect Cleanup Order:**
|
|
```cpp
|
|
// ✅ GOOD: Correct cleanup order (handles before comm)
|
|
TEST_F(MyTest, Example) {
|
|
ncclComm_t comm;
|
|
ncclCommInitRank(&comm, ...);
|
|
auto comm_guard = makeCommAutoGuard(comm);
|
|
|
|
void* reg_handle;
|
|
ncclCommRegister(comm, buffer, size, ®_handle);
|
|
auto handle_guard = makeRegHandleGuard(reg_handle, comm);
|
|
|
|
// Cleanup order: handle_guard destroyed first (correct!)
|
|
// Then comm_guard destroyed
|
|
}
|
|
|
|
// ❌ BAD: Wrong order causes "corrupted comm object" error
|
|
TEST_F(MyTest, Example) {
|
|
void* reg_handle;
|
|
ncclCommRegister(comm, buffer, size, ®_handle);
|
|
auto handle_guard = makeRegHandleGuard(reg_handle, comm);
|
|
|
|
ncclComm_t comm;
|
|
ncclCommInitRank(&comm, ...);
|
|
auto comm_guard = makeCommAutoGuard(comm);
|
|
|
|
// Cleanup order: comm_guard destroyed first - ERROR!
|
|
// handle_guard tries to use destroyed comm
|
|
}
|
|
```
|
|
|
|
**3. Use Local Guards for Loops:**
|
|
```cpp
|
|
// ✅ GOOD: Local guards for per-iteration cleanup
|
|
for (const auto size : test_sizes) {
|
|
void* buffer;
|
|
hipMalloc(&buffer, size);
|
|
auto guard = makeDeviceBufferAutoGuard(buffer);
|
|
|
|
// Test with this size...
|
|
|
|
// Buffer freed here at end of iteration
|
|
}
|
|
|
|
// ❌ BAD: Accumulating allocations
|
|
std::vector<void*> buffers;
|
|
for (const auto size : test_sizes) {
|
|
void* buffer;
|
|
hipMalloc(&buffer, size);
|
|
buffers.push_back(buffer); // Memory accumulates!
|
|
}
|
|
// All buffers freed only at end - high memory usage
|
|
```
|
|
|
|
**4. Use Custom Guards for Lambdas:**
|
|
```cpp
|
|
// Complex cleanup with lambda
|
|
FILE* file = fopen("test.txt", "w");
|
|
auto guard = makeCustomGuard(file, [](FILE* f) {
|
|
if (f) {
|
|
fflush(f);
|
|
fclose(f);
|
|
}
|
|
});
|
|
// file automatically flushed and closed on scope exit
|
|
```
|
|
|
|
### Implementation Details
|
|
|
|
**AutoGuard:**
|
|
- Uses C++17 `auto` non-type template parameters
|
|
- Zero overhead - deleter is a compile-time constant
|
|
- No functor object stored - just the resource handle
|
|
- Smaller memory footprint than ResourceGuard
|
|
|
|
**ResourceGuard:**
|
|
- Stores both resource and deleter functor
|
|
- Supports stateful deleters with additional context
|
|
- Move-only semantics (non-copyable)
|
|
- Slightly larger memory footprint due to deleter storage
|
|
|
|
**When to Use Which:**
|
|
- Use **AutoGuard** (via factory methods like `makeDeviceBufferAutoGuard`) for simple cleanup
|
|
- Use **ResourceGuard** (via factory methods like `makeRegHandleGuard`) for cleanup requiring context
|
|
|
|
### See Also
|
|
|
|
- **ResourceGuards.hpp** - Full guard implementation (includes ScopeGuard, AutoGuard, ResourceGuard)
|
|
- **TransportMPIBase.hpp** - Transport test base with guarded resource management
|
|
- **Best Practices** section above for RAII usage patterns
|
|
|
|
---
|
|
|
|
## Device Buffer Helpers
|
|
|
|
The test infrastructure provides template-based device buffer utilities with a clean, pluggable API for initialization and verification. These helpers eliminate code duplication and provide type-safe operations for all NCCL data types.
|
|
|
|
**Location:** `test/common/DeviceBufferHelpers.hpp`
|
|
|
|
### Overview
|
|
|
|
**Key Features:**
|
|
- ✅ **Pluggable patterns** - Use lambdas to define any initialization/verification pattern
|
|
- ✅ **Type-safe** - Template-based with automatic NCCL type mapping
|
|
- ✅ **Automatic float/int comparison** - Correct comparison logic based on type
|
|
|
|
### Type Mapping
|
|
|
|
NCCL type traits automatically map C++ types to `ncclDataType_t`:
|
|
|
|
```cpp
|
|
// Supported types (implemented using stringification macro)
|
|
float → ncclFloat
|
|
double → ncclDouble
|
|
int8_t → ncclInt8
|
|
uint8_t → ncclUint8
|
|
int32_t → ncclInt32
|
|
uint32_t → ncclUint32
|
|
int64_t → ncclInt64
|
|
uint64_t → ncclUint64
|
|
|
|
// Usage (type deduced automatically)
|
|
ncclDataType_t type = getNcclDataType<float>(); // ncclFloat
|
|
const char* name = getTypeName<uint64_t>(); // "uint64_t"
|
|
```
|
|
|
|
### Buffer Initialization
|
|
|
|
Generic initialization with pattern functions:
|
|
|
|
```cpp
|
|
// Signature
|
|
template<typename T, typename PatternFunc>
|
|
hipError_t initializeBufferWithPattern(
|
|
void* device_buffer,
|
|
size_t num_elements,
|
|
PatternFunc pattern_func);
|
|
|
|
// Example 1: Rank-based pattern
|
|
initializeBufferWithPattern<float>(
|
|
buffer, size,
|
|
[rank, multiplier](size_t i) {
|
|
return static_cast<float>(rank * multiplier + i);
|
|
});
|
|
|
|
// Example 2: Constant value
|
|
initializeBufferWithPattern<int>(
|
|
buffer, size,
|
|
[](size_t i) { return 42; });
|
|
|
|
// Example 3: Custom pattern with modulo
|
|
initializeBufferWithPattern<uint8_t>(
|
|
buffer, size,
|
|
[pattern](size_t i) {
|
|
return static_cast<uint8_t>((pattern + i) % 256);
|
|
});
|
|
```
|
|
|
|
### Buffer Verification
|
|
|
|
Generic verification with pattern functions:
|
|
|
|
```cpp
|
|
// Signature
|
|
template<typename T, typename PatternFunc>
|
|
bool verifyBufferData(
|
|
const void* device_buffer,
|
|
size_t num_elements,
|
|
PatternFunc pattern_func,
|
|
size_t num_samples = 0, // 0 = verify all
|
|
double tolerance = 1e-5,
|
|
size_t* first_error_index = nullptr,
|
|
T* expected_value = nullptr,
|
|
T* actual_value = nullptr);
|
|
|
|
// Example 1: Rank-based verification
|
|
size_t error_idx;
|
|
float expected_val, actual_val;
|
|
bool ok = verifyBufferData<float>(
|
|
recv_buffer, count,
|
|
[peer_rank](size_t i) {
|
|
return static_cast<float>(peer_rank * 1000 + i);
|
|
},
|
|
10, // verify first 10 elements
|
|
1e-5, // tolerance for floats
|
|
&error_idx, &expected_val, &actual_val);
|
|
|
|
// Example 2: Verify all elements
|
|
bool ok = verifyBufferData<int>(
|
|
buffer, size,
|
|
[](size_t i) { return static_cast<int>(i * 2); },
|
|
0); // 0 = verify ALL elements
|
|
|
|
// Example 3: Custom pattern
|
|
bool ok = verifyBufferData<uint8_t>(
|
|
buffer, size,
|
|
[pattern](size_t i) {
|
|
return static_cast<uint8_t>((pattern + i) % 256);
|
|
});
|
|
```
|
|
|
|
### Key Features
|
|
|
|
**1. Pluggable Pattern Logic:**
|
|
The pattern is defined at the call site as a lambda, making the code self-documenting:
|
|
```cpp
|
|
// ✅ Pattern logic is immediately visible
|
|
verifyBufferData<float>(buffer, size,
|
|
[rank](size_t i) { return rank * 1000 + i; }); // Clear what we expect
|
|
|
|
// vs old approach (pattern logic hidden in function params)
|
|
// verifyBufferData<float>(buffer, size, rank, 1000, ...); // Less clear
|
|
```
|
|
|
|
**2. Automatic Float vs Int Comparison:**
|
|
The helpers automatically use the correct comparison for the data type:
|
|
```cpp
|
|
// For float/double: uses tolerance-based comparison
|
|
verifyBufferData<float>(buffer, size, pattern_func, 10, 1e-5);
|
|
|
|
// For int types: uses exact comparison (tolerance ignored)
|
|
verifyBufferData<int32_t>(buffer, size, pattern_func);
|
|
```
|
|
|
|
**3. Flexible Verification:**
|
|
```cpp
|
|
// Verify first 10 elements (fast sampling)
|
|
verifyBufferData<float>(buffer, size, pattern, 10);
|
|
|
|
// Verify ALL elements (comprehensive)
|
|
verifyBufferData<float>(buffer, size, pattern, 0); // 0 = all
|
|
|
|
// Get error details
|
|
size_t error_idx;
|
|
T expected, actual;
|
|
if (!verifyBufferData<T>(buffer, size, pattern, 0, 1e-5,
|
|
&error_idx, &expected, &actual)) {
|
|
printf("Mismatch at index %zu: expected %f, got %f\n",
|
|
error_idx, expected, actual);
|
|
}
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### MPI Initialization Failed
|
|
|
|
**Symptom:** `MPIEnvironment::retCode != 0` or "Only X GPUs available for Y ranks"
|
|
|
|
**Causes:**
|
|
- Insufficient GPUs for the number of **local** processes (per node)
|
|
- MPI not properly installed
|
|
- Wrong MPI launcher configuration
|
|
|
|
**Solutions:**
|
|
```cpp
|
|
// Check in test
|
|
if (MPIEnvironment::retCode != 0) {
|
|
GTEST_SKIP() << "MPI initialization failed";
|
|
}
|
|
|
|
// Or use validateTestPrerequisites which checks this
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
```
|
|
|
|
```bash
|
|
# For single-node: GPU count must match process count
|
|
mpirun -np 8 -x HIP_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 ./test_executable
|
|
|
|
# For multi-node: GPU count per node must match processes per node
|
|
# Example: 16 ranks on 2 nodes = 8 ranks/node, need 8 GPUs/node
|
|
salloc -N 2 --gres=gpu:8 --ntasks-per-node=8
|
|
mpirun -np 16 ./test_executable
|
|
|
|
# Check GPU availability on each node
|
|
rocm-smi --showid
|
|
```
|
|
|
|
**Multi-Node GPU Assignment:**
|
|
The framework now uses **local rank** (rank within node) for GPU assignment, not global rank. This means:
|
|
- Node 1: Ranks 0-7 use GPUs 0-7
|
|
- Node 2: Ranks 8-15 use GPUs 0-7
|
|
- Each node independently assigns its local ranks to its local GPUs
|
|
|
|
### Test Hangs / Deadlock
|
|
|
|
**Symptom:** Test never completes, all ranks waiting.
|
|
|
|
**Debugging:** Enable per-rank logging to see where each rank gets stuck:
|
|
```bash
|
|
# Run with per-rank logging
|
|
export RCCL_MPI_LOG_ALL_RANKS=1
|
|
mpirun -np 4 ./rccl-UnitTestsMPI --gtest_filter="HangingTest.*" &
|
|
|
|
# Monitor logs in real-time to see where each rank stops
|
|
tail -f rccl_test_rank_*.log
|
|
```
|
|
|
|
**Common Causes:**
|
|
|
|
1. **Mismatched Collectives:**
|
|
```cpp
|
|
// ❌ BAD: Not all ranks participate
|
|
if (rank == 0) {
|
|
ncclAllReduce(...); // Other ranks don't call this!
|
|
}
|
|
|
|
// ✅ GOOD: All ranks participate
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclAllReduce(...));
|
|
```
|
|
|
|
2. **Missing Synchronization:**
|
|
```cpp
|
|
// ❌ BAD: Rank 0 waits for data other ranks haven't sent
|
|
ASSERT_MPI_SUCCESS(MPI_Barrier(MPI_COMM_WORLD)); // All ranks must reach this
|
|
|
|
// ✅ GOOD: createTestCommunicator() includes barriers
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
```
|
|
|
|
3. **Stream Not Synchronized:**
|
|
```cpp
|
|
// ✅ GOOD: Wait for operations to complete
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipStreamSynchronize(getActiveStream()));
|
|
ASSERT_MPI_SUCCESS(MPI_Barrier(MPI_COMM_WORLD));
|
|
```
|
|
|
|
**See:** [Per-Rank Logging](#per-rank-logging) for detailed debugging techniques
|
|
|
|
### "More Processes Than CPUs" Error
|
|
|
|
**Symptom:**
|
|
```
|
|
A request was made to bind to that would result in binding more
|
|
processes than cpus available in your allocation:
|
|
#processes: 16
|
|
Binding policy: CORE
|
|
```
|
|
|
|
**Cause:** OpenMPI trying to bind each process to a dedicated CPU core, but insufficient cores.
|
|
|
|
**Solution:** The test runner automatically uses `--bind-to none` to disable CPU binding.
|
|
|
|
```bash
|
|
# Automatic (if using test runner script)
|
|
./build_test_coverage.std.sh --config test_config.txt
|
|
|
|
# Manual fix: add --bind-to none
|
|
mpirun -np 16 --bind-to none ./test_executable
|
|
```
|
|
|
|
**Why this works:** For GPU tests, CPU binding is not critical since GPUs do the computation. Disabling binding allows any number of processes regardless of CPU count.
|
|
|
|
### Hostfile Issues / "Host Not Found"
|
|
|
|
**Symptom:**
|
|
```
|
|
Missing requested host: node-name
|
|
At least one of the requested hosts is not included in the current allocation
|
|
```
|
|
|
|
**Cause:** Hostfile doesn't match allocated nodes or has wrong format.
|
|
|
|
**Solution:** The test runner automatically generates hostfiles from SLURM allocations.
|
|
|
|
```bash
|
|
# Automatic (if using test runner with SLURM)
|
|
salloc -N 2 -n 16
|
|
./build_test_coverage.std.sh --config test_config.txt --no-build
|
|
# Creates temporary hostfile:
|
|
# node-3 slots=8
|
|
# node-21 slots=8
|
|
|
|
# Manual hostfile creation
|
|
cat > hostfile.txt << EOF
|
|
node-3 slots=8
|
|
node-21 slots=8
|
|
EOF
|
|
mpirun -np 16 --hostfile hostfile.txt ./test_executable
|
|
```
|
|
|
|
**Hostfile format:**
|
|
- `hostname slots=N` where N is max processes per node
|
|
- Script auto-calculates: `slots = total_ranks / num_nodes`
|
|
|
|
### Rank Mismatch in ncclCommInitRank
|
|
|
|
**Symptom:** Error during communicator creation.
|
|
|
|
**Solution:**
|
|
```cpp
|
|
// ✅ GOOD: Use createTestCommunicator()
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
// ❌ BAD: Manual initialization can get ranks wrong
|
|
ncclCommInitRank(&comm, size, id, wrong_rank);
|
|
```
|
|
|
|
### GPU Out of Memory
|
|
|
|
**Symptom:** `hipMalloc()` fails with memory error.
|
|
|
|
**Solutions:**
|
|
1. Reduce buffer sizes
|
|
2. Ensure proper cleanup of previous allocations
|
|
3. Use RAII guards to prevent leaks on test failure
|
|
4. Run tests sequentially instead of in parallel
|
|
|
|
```cpp
|
|
// ✅ GOOD: Use RAII guards (automatic cleanup even on failure)
|
|
TEST_F(MyTest, Example) {
|
|
void* buffer = nullptr;
|
|
allocateAndInitBuffersGuarded(&buffer, nullptr, size, 0);
|
|
// Automatic cleanup even if test fails
|
|
}
|
|
|
|
// ❌ BAD: Manual cleanup in TearDown (leaks if test fails)
|
|
void TearDown() override {
|
|
if (buffer) {
|
|
hipFree(buffer);
|
|
buffer = nullptr;
|
|
}
|
|
MPITestBase::TearDown();
|
|
}
|
|
```
|
|
|
|
### Corrupted Comm Object Error
|
|
|
|
**Symptom:** Test passes but logs "corrupted comm object detected" errors during cleanup:
|
|
```
|
|
NCCL WARN Error: corrupted comm object detected
|
|
/home/.../src/register/register.cc:171 -> 4
|
|
```
|
|
|
|
**Cause:** Registration handles being deregistered after communicator was destroyed.
|
|
|
|
**Solution:** Use `preRegisterBuffersGuarded()` instead of manual registration:
|
|
|
|
```cpp
|
|
// ✅ GOOD: Guards ensure proper cleanup order
|
|
TEST_F(MyTransportTest, Example) {
|
|
void* send_buffer = nullptr;
|
|
void* recv_buffer = nullptr;
|
|
allocateAndInitBuffersGuarded(&send_buffer, &recv_buffer, size, size);
|
|
|
|
void* send_handle = nullptr;
|
|
void* recv_handle = nullptr;
|
|
preRegisterBuffersGuarded(send_buffer, recv_buffer, size, size,
|
|
&send_handle, &recv_handle);
|
|
|
|
// Guards automatically deregister handles BEFORE comm is destroyed
|
|
}
|
|
|
|
// ❌ BAD: Manual deregistration may happen after comm destroyed
|
|
TEST_F(MyTest, Example) {
|
|
void* handle = nullptr;
|
|
ncclCommRegister(comm, buffer, size, &handle);
|
|
// ... test logic ...
|
|
ncclCommDeregister(comm, handle); // May fail if comm destroyed first!
|
|
}
|
|
```
|
|
|
|
**Why it works:** `TransportTestBase::TearDown()` explicitly clears guard vectors before
|
|
destroying the communicator, ensuring handles are deregistered while the communicator is
|
|
still valid.
|
|
|
|
### Test Skipped: Not Enough Processes
|
|
|
|
**Symptom:** Test skipped with message about process count.
|
|
|
|
**Solution:** Run with more processes:
|
|
```bash
|
|
# Test requires 4 processes
|
|
mpirun -np 4 ./test_executable --gtest_filter="MyTest.SomeTest"
|
|
|
|
# Check what test needs
|
|
grep validateTestPrerequisites test_file.cpp
|
|
```
|
|
|
|
### Test Requires Multi-Node But Detects Single Node
|
|
|
|
**Symptom:** Test skipped with message:
|
|
```
|
|
Error: REQUIREMENT NOT MET: Need at least 2 node(s), detected 1 nodes
|
|
```
|
|
|
|
**Root Cause:** Your `mpirun` command launched all processes on the local node, so node detection correctly reports 1 node. **The detection is working correctly - your launch command needs fixing!**
|
|
|
|
**Why this happens:**
|
|
```bash
|
|
# ❌ This launches ALL processes on the node where you run the command
|
|
mpirun -np 16 ./test_executable
|
|
# Even with a 2-node SLURM allocation, this puts all 16 processes on the local node
|
|
```
|
|
|
|
**Solutions - You MUST distribute processes across nodes:**
|
|
|
|
**Option 1: Use SLURM's srun (Recommended)**
|
|
```bash
|
|
# SLURM's srun automatically distributes based on your allocation
|
|
salloc -N 2 --ntasks-per-node=8
|
|
srun -N 2 -n 16 ./test_executable
|
|
# ✓ Node detection: 2 nodes
|
|
```
|
|
|
|
**Option 2: Use hostfile with proper node names**
|
|
```bash
|
|
# Create hostfile from SLURM allocation
|
|
scontrol show hostnames $SLURM_JOB_NODELIST > hostfile.txt
|
|
|
|
# Or create manually with actual node names:
|
|
cat > hostfile.txt << EOF
|
|
node-3 slots=8
|
|
node-21 slots=8
|
|
EOF
|
|
|
|
mpirun -np 16 --hostfile hostfile.txt ./test_executable
|
|
# ✓ Node detection: 2 nodes
|
|
```
|
|
|
|
**Option 3: Explicit host specification**
|
|
```bash
|
|
# Get your allocated node names
|
|
NODE1=$(scontrol show hostnames $SLURM_JOB_NODELIST | sed -n '1p')
|
|
NODE2=$(scontrol show hostnames $SLURM_JOB_NODELIST | sed -n '2p')
|
|
|
|
# Distribute explicitly
|
|
mpirun -np 16 --host ${NODE1}:8,${NODE2}:8 ./test_executable
|
|
# ✓ Node detection: 2 nodes
|
|
```
|
|
|
|
**Option 4: Use distribution policy**
|
|
```bash
|
|
# Requires OpenMPI with SLURM support
|
|
mpirun -np 16 --map-by ppr:8:node ./test_executable
|
|
# ✓ Node detection: 2 nodes
|
|
```
|
|
|
|
**Verify your distribution before running tests:**
|
|
```bash
|
|
# This should show different hostnames
|
|
mpirun -np 16 --host ${NODE1}:8,${NODE2}:8 hostname
|
|
# Should print: 8 lines with node1, 8 lines with node2
|
|
|
|
# Or with srun:
|
|
srun -N 2 -n 16 hostname
|
|
```
|
|
|
|
**Key Point:** Node detection reports WHERE processes ARE running, not where you have nodes allocated. Fix your launch command to actually distribute processes!
|
|
|
|
### Test Skipped: Node Requirement Not Met
|
|
|
|
**Symptom:** Test skipped with message like:
|
|
```
|
|
Skipping test - requires at most 1 node(s), detected 2 nodes
|
|
This test requires single-node execution
|
|
To run on single node, allocate all processes on the same host
|
|
```
|
|
|
|
**Cause:** Test requires single-node execution but was run on multiple nodes.
|
|
|
|
**Solutions:**
|
|
|
|
**Option 1: Run on single node with multiple GPUs**
|
|
```bash
|
|
# Run 2 processes on single node
|
|
mpirun -np 2 ./test_executable --gtest_filter="SingleNodeTest.*"
|
|
|
|
# Or specify GPUs explicitly
|
|
mpirun -np 2 -x HIP_VISIBLE_DEVICES=0,1 ./test_executable
|
|
```
|
|
|
|
**Option 2: Use multi-node capable tests instead**
|
|
```bash
|
|
# Run tests that work across multiple nodes
|
|
mpirun -np 4 --host node1,node2 ./test_executable --gtest_filter="MultiNodeTest.*"
|
|
```
|
|
|
|
**Option 3: Check your hostfile/job allocation**
|
|
```bash
|
|
# Verify you're actually on different nodes
|
|
mpirun -np 2 hostname
|
|
|
|
# If both print same hostname, you're on one node
|
|
# If different hostnames, you're on multiple nodes
|
|
```
|
|
|
|
### Test Fails on Multi-Node But Works on Single-Node
|
|
|
|
**Symptom:**
|
|
- Test passes when all processes are on one node
|
|
- Test fails or crashes when processes are on different nodes
|
|
- May see errors like "invalid device pointer", "segmentation fault", or hangs
|
|
|
|
**Cause:** Test is using features that require single-node execution but lacks node validation.
|
|
|
|
**Common single-node features:**
|
|
- Direct GPU-to-GPU memory access (P2P transport)
|
|
- Shared memory between processes (SHM transport)
|
|
- Inter-process communication (IPC) handles
|
|
- Assumptions about memory locality or GPU topology
|
|
|
|
**Solution:**
|
|
```cpp
|
|
// ✅ GOOD: Add node validation
|
|
TEST_F(MyTest, LocalFeature) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kRequireSingleNode));
|
|
// Test will skip gracefully on multi-node
|
|
}
|
|
|
|
// ✅ GOOD: Or redesign to use network-capable features
|
|
TEST_F(MyTest, DistributedFeature) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
// Uses defaults: any nodes allowed
|
|
// Use NET transport or network-based communication
|
|
}
|
|
```
|
|
|
|
### Wrong Results Across Ranks
|
|
|
|
**Symptom:** Different ranks get different results.
|
|
|
|
**Solution:** Enable per-rank logging to see output from all ranks:
|
|
```bash
|
|
# Enable per-rank logging for detailed debugging
|
|
export RCCL_MPI_LOG_ALL_RANKS=1
|
|
mpirun -np 4 ./rccl-UnitTestsMPI --gtest_filter="FailingTest.*"
|
|
|
|
# Check logs from each rank
|
|
cat rccl_test_rank_0.log
|
|
cat rccl_test_rank_1.log
|
|
```
|
|
|
|
**Debugging:**
|
|
```cpp
|
|
// Use TEST_INFO for debug output (respects NCCL_DEBUG=INFO)
|
|
TEST_INFO("result[0] = %f", result[0]);
|
|
ASSERT_MPI_SUCCESS(MPI_Barrier(MPI_COMM_WORLD));
|
|
|
|
// Verify all ranks agree
|
|
float local_result = result[0];
|
|
float global_result;
|
|
ASSERT_MPI_SUCCESS(MPI_Allreduce(&local_result, &global_result, 1, MPI_FLOAT,
|
|
MPI_MAX, MPI_COMM_WORLD));
|
|
EXPECT_FLOAT_EQ(local_result, global_result);
|
|
```
|
|
|
|
**Debug with NCCL_DEBUG:**
|
|
```bash
|
|
# Enable detailed logging from tests and library
|
|
NCCL_DEBUG=INFO RCCL_MPI_LOG_ALL_RANKS=1 mpirun -np 4 ./test_executable
|
|
|
|
# Output shows rank/hostname automatically:
|
|
# [0] TEST INFO result[0] = 1.000000
|
|
# [1] TEST INFO result[0] = 2.000000
|
|
# [2] TEST INFO result[0] = 3.000000
|
|
```
|
|
|
|
**See:** [Per-Rank Logging](#per-rank-logging) for more details
|
|
|
|
---
|
|
|
|
## Advanced Topics
|
|
|
|
### Extending MPITestBase
|
|
|
|
Create specialized base classes for specific test types:
|
|
|
|
```cpp
|
|
class TransportTestBase : public MPITestBase {
|
|
protected:
|
|
ncclConnector send_connector = {};
|
|
ncclConnector recv_connector = {};
|
|
|
|
void initializeTransport() {
|
|
// Transport-specific setup
|
|
}
|
|
|
|
void SetUp() override {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
initializeTransport();
|
|
}
|
|
};
|
|
|
|
TEST_F(TransportTestBase, P2PTransfer) {
|
|
// Transport already initialized
|
|
// Just write your test logic
|
|
}
|
|
```
|
|
|
|
### Testing with Multiple Communicators
|
|
|
|
```cpp
|
|
TEST_F(MyTest, MultipleComms) {
|
|
ASSERT_TRUE(validateTestPrerequisites(4));
|
|
|
|
// Create two separate communicators
|
|
ncclUniqueId id1, id2;
|
|
ncclComm_t comm1 = nullptr, comm2 = nullptr;
|
|
|
|
if (MPIEnvironment::world_rank == 0) {
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclGetUniqueId(&id1));
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclGetUniqueId(&id2));
|
|
}
|
|
|
|
ASSERT_MPI_SUCCESS(MPI_Bcast(&id1, sizeof(id1), MPI_BYTE, 0, MPI_COMM_WORLD));
|
|
ASSERT_MPI_SUCCESS(MPI_Bcast(&id2, sizeof(id2), MPI_BYTE, 0, MPI_COMM_WORLD));
|
|
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclCommInitRank(&comm1, MPIEnvironment::world_size, id1,
|
|
MPIEnvironment::world_rank));
|
|
auto comm1_guard = makeCommAutoGuard(comm1);
|
|
|
|
RCCL_TEST_CHECK_GTEST_FAIL(ncclCommInitRank(&comm2, MPIEnvironment::world_size, id2,
|
|
MPIEnvironment::world_rank));
|
|
auto comm2_guard = makeCommAutoGuard(comm2);
|
|
|
|
// Use both communicators...
|
|
|
|
// Automatic cleanup via RAII guards
|
|
}
|
|
```
|
|
|
|
### Testing Error Conditions
|
|
|
|
```cpp
|
|
TEST_F(MyTest, InvalidRankHandling) {
|
|
ASSERT_TRUE(validateTestPrerequisites(kMinProcessesForMPI));
|
|
ASSERT_EQ(ncclSuccess, createTestCommunicator());
|
|
|
|
void* buffer = nullptr;
|
|
HIP_TEST_CHECK_GTEST_FAIL(hipMalloc(&buffer, 1024));
|
|
auto buffer_guard = makeDeviceBufferAutoGuard(buffer);
|
|
|
|
// Deliberately use invalid rank
|
|
int invalid_rank = 999;
|
|
|
|
// Expect error (don't crash)
|
|
ncclResult_t result = ncclSend(buffer, 256, ncclFloat, invalid_rank,
|
|
getActiveCommunicator(), getActiveStream());
|
|
|
|
EXPECT_NE(result, ncclSuccess);
|
|
// Test should continue, not deadlock
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## FAQ
|
|
|
|
**Q: When should I use MPITestBase vs ProcessIsolatedTestRunner?**
|
|
|
|
A: Choose based on your testing requirements:
|
|
|
|
**Use MPITestBase when:**
|
|
- ✅ Testing **multi-process** RCCL operations (collectives, point-to-point)
|
|
- ✅ Testing **multi-node** distributed execution
|
|
- ✅ Validating communication between multiple GPUs/ranks
|
|
- ✅ Testing transport layers (P2P, SHM, NET)
|
|
- ✅ Verifying scalability across processes and nodes
|
|
- ✅ Testing MPI-based coordination and synchronization
|
|
- **Examples:** AllReduce, Broadcast, Send/Recv, multi-GPU collectives, cross-node communication
|
|
|
|
**Use ProcessIsolatedTestRunner when:**
|
|
- ✅ Testing **single-process** code with clean environment isolation
|
|
- ✅ Need to **set environment variables programmatically** for each test
|
|
- ✅ Testing environment-dependent behavior without affecting other tests
|
|
- ✅ Validating RCCL configuration with different environment settings
|
|
- ✅ Testing initialization/cleanup with isolated state
|
|
- ✅ Running tests that require specific environment variables
|
|
- **Examples:** Testing NCCL_DEBUG levels, NCCL tuning parameters, plugin loading, environment-specific initialization
|
|
|
|
**Key Differences:**
|
|
|
|
| Feature | MPITestBase | ProcessIsolatedTestRunner |
|
|
|---------|-------------|---------------------------|
|
|
| **Process Count** | Multiple (2+) | Single |
|
|
| **Node Support** | Single or multi-node | Single node only |
|
|
| **Environment Control** | Inherited from shell | Programmatic per test |
|
|
| **Use Case** | Multi-GPU/multi-node operations | Environment-dependent single-process tests |
|
|
| **Coordination** | MPI barriers and communication | None (isolated process) |
|
|
|
|
**Q: How many processes should I test with?**
|
|
|
|
A:
|
|
- Minimum: 2 (for basic collectives and P2P)
|
|
- Common: 2, 4, 8 (good coverage)
|
|
- For scalability: Test with various counts
|
|
- For algorithms: Some require power-of-two (use `kRequirePowerOfTwo`)
|
|
|
|
**Q: Does `validateTestPrerequisites()` work correctly across multiple nodes?**
|
|
|
|
A: Yes! It validates both process count AND node count:
|
|
- **Process count validation**: Checks total number of processes (any node distribution)
|
|
- **Node count validation**: Detects number of unique nodes via hostnames
|
|
- Tests can specify node requirements based on what they need
|
|
|
|
Example:
|
|
```cpp
|
|
// Test that works on any number of nodes (uses defaults)
|
|
validateTestPrerequisites(2);
|
|
|
|
// Test that requires single-node execution
|
|
validateTestPrerequisites(2, kNoProcessLimit, kNoPowerOfTwoRequired, 1, kRequireSingleNode);
|
|
```
|
|
|
|
**Q: How does node detection work?**
|
|
|
|
A: Node detection uses MPI's built-in shared memory domain detection:
|
|
1. `MPI_Comm_split_type()` with `MPI_COMM_TYPE_SHARED` splits ranks by node (shared memory domain)
|
|
2. Each rank determines its local rank and node size (ranks per node)
|
|
3. Node sizes are gathered to rank 0
|
|
4. Rank 0 analyzes the distribution to count unique nodes
|
|
5. Node count is broadcast to all ranks
|
|
|
|
**Critical: Node detection reports actual process placement, not intent.**
|
|
|
|
The detection method is robust and works with any MPI implementation. However, it detects WHERE processes are actually running. If your `mpirun` command doesn't distribute processes across nodes, detection correctly reports 1 node.
|
|
|
|
**Common mistake:**
|
|
```bash
|
|
# This launches all processes on the LOCAL node (where you run the command)
|
|
mpirun -np 16 ./test_executable
|
|
# Node detection: 1 node ✓ (correct - all on one node)
|
|
```
|
|
|
|
**Correct multi-node launch:**
|
|
```bash
|
|
# With SLURM (recommended - automatic distribution)
|
|
srun -N 2 -n 16 ./test_executable
|
|
|
|
# With hostfile
|
|
mpirun -np 16 --hostfile hostfile.txt ./test_executable
|
|
|
|
# With explicit hosts
|
|
mpirun -np 16 --host node1:8,node2:8 ./test_executable
|
|
|
|
# With distribution policy
|
|
mpirun -np 16 --map-by ppr:8:node ./test_executable
|
|
# Node detection: 2 nodes ✓ (processes actually on 2 nodes)
|
|
```
|
|
|
|
The detection method works transparently with any job scheduler (SLURM, PBS, etc.), but **you must launch processes correctly** for them to actually be distributed across nodes.
|
|
|
|
**Q: When should I use `kRequireSingleNode` vs `kNoNodeLimit`?**
|
|
|
|
A: Based on what your test requires:
|
|
|
|
**Use `kRequireSingleNode` when your test:**
|
|
- Uses direct GPU-to-GPU memory access
|
|
- Requires shared memory between processes
|
|
- Uses inter-process communication (IPC) handles
|
|
- Makes assumptions about memory locality or GPU topology
|
|
- Uses features that only work within a single physical node
|
|
- Examples: P2P transport, SHM transport, GPU topology tests, local memory tests
|
|
|
|
**Use `kNoNodeLimit` (default) when your test:**
|
|
- Works with network-based communication
|
|
- Uses distributed features or algorithms
|
|
- Should work regardless of node configuration
|
|
- Tests scalability across multiple nodes
|
|
- Examples: NET transport, collective operations, RDMA features, scalability tests
|
|
|
|
**General guidance:**
|
|
- If your test uses intra-node features → use `kRequireSingleNode`
|
|
- If your test works across nodes → use `kNoNodeLimit` or omit (it's the default)
|
|
- If unsure, leave it as default (`kNoNodeLimit`) and add validation if you encounter multi-node issues
|
|
|
|
**Q: Can I run MPI tests locally?**
|
|
|
|
A: Yes, if you have:
|
|
- Multiple GPUs in your system
|
|
- MPI installed (OpenMPI, MPICH, etc.)
|
|
- Tests built with `MPI_TESTS_ENABLED`
|
|
|
|
**Q: How do I debug a specific rank?**
|
|
|
|
A:
|
|
```bash
|
|
# Method 1: Use per-rank logging with NCCL_DEBUG (easiest)
|
|
NCCL_DEBUG=INFO RCCL_MPI_LOG_ALL_RANKS=1 mpirun -np 4 ./test_executable
|
|
# Check rccl_test_rank_2.log for rank 2 output
|
|
# TEST_INFO messages will appear automatically
|
|
|
|
# Method 2: GDB with MPI
|
|
mpirun -np 2 xterm -e gdb ./test_executable
|
|
|
|
# Method 3: Attach to specific rank PID
|
|
mpirun -np 4 ./test_executable &
|
|
gdb -p <rank_pid>
|
|
|
|
# Method 4: Use TEST_INFO for conditional debugging
|
|
if (MPIEnvironment::world_rank == 2) {
|
|
TEST_INFO("Debug info from rank 2...");
|
|
// Only appears when NCCL_DEBUG=INFO
|
|
}
|
|
```
|
|
|
|
**Q: How do TEST_* macros work with NCCL_DEBUG?**
|
|
|
|
A: TEST_* macros respect the `NCCL_DEBUG` environment variable:
|
|
```bash
|
|
# No output from TEST_* macros (clean)
|
|
mpirun -np 2 ./test_executable
|
|
|
|
# TEST_INFO and higher appear
|
|
NCCL_DEBUG=INFO mpirun -np 2 ./test_executable
|
|
|
|
# All TEST_* macros appear
|
|
NCCL_DEBUG=TRACE mpirun -np 2 ./test_executable
|
|
```
|
|
|
|
**Available levels (least to most verbose):**
|
|
- `NCCL_DEBUG=WARN` → TEST_WARN
|
|
- `NCCL_DEBUG=INFO` → TEST_WARN, TEST_INFO (recommended for debugging)
|
|
- `NCCL_DEBUG=ABORT` → All above + TEST_ABORT
|
|
- `NCCL_DEBUG=TRACE` → All macros including TEST_TRACE
|
|
|
|
**Benefits:**
|
|
- ✅ Same verbosity control as RCCL library
|
|
- ✅ Automatic rank prefixes (no manual "Rank %d:")
|
|
- ✅ Hostname included for multi-node tests
|
|
- ✅ Clean output in production (no NCCL_DEBUG)
|
|
- ✅ Detailed debugging on demand (NCCL_DEBUG=INFO)
|
|
|
|
---
|
|
|
|
## Standalone Tests
|
|
|
|
The MPI test infrastructure now supports **framework-agnostic testing**, allowing you to write tests
|
|
without Google Test. This is ideal for:
|
|
|
|
- **Performance benchmarks** (bandwidth, latency)
|
|
- **Low-level API tests** without GTest overhead
|
|
- **Production utilities** using MPI infrastructure
|
|
- **Custom test harnesses**
|
|
|
|
### Quick Comparison
|
|
|
|
| Feature | GTest (MPITestBase) | Standalone (MPIStandaloneTest) |
|
|
|---------|--------------------|---------------------------------|
|
|
| Requires GTest | ✅ Yes | ❌ No |
|
|
| Assertions | ASSERT_*, EXPECT_* | Return codes |
|
|
| Setup/Teardown | Automatic | Manual `cleanup()` |
|
|
| Best For | Unit/integration tests | Performance benchmarks |
|
|
| Overhead | Higher | Minimal |
|
|
|
|
### Example: Standalone Test
|
|
|
|
```cpp
|
|
#include "MPIStandaloneTest.hpp"
|
|
#include "MPIHelpers.hpp"
|
|
|
|
class MyBenchmark : public MPIStandaloneTest {
|
|
public:
|
|
int run() override {
|
|
// Validate prerequisites
|
|
if (!validateTestPrerequisites(2, 2)) return 0; // Skip
|
|
|
|
// Create communicator
|
|
if (createTestCommunicator() != ncclSuccess) return 1; // Error
|
|
|
|
// Your benchmark code here...
|
|
// Use getActiveCommunicator() and getActiveStream()
|
|
|
|
return 0; // Success
|
|
}
|
|
};
|
|
|
|
int main(int argc, char** argv) {
|
|
// Initialize MPI and setup GPU
|
|
auto mpi_ctx = MPIHelpers::initializeMPI(&argc, &argv);
|
|
MPIHelpers::setupGPU(mpi_ctx.world_rank);
|
|
|
|
// Run test with automatic cleanup via RAII
|
|
int result = 0;
|
|
{
|
|
MyBenchmark test;
|
|
MPIStandaloneTestRAII raii(&test); // Automatic cleanup on scope exit
|
|
result = test.run();
|
|
}
|
|
|
|
MPI_Finalize();
|
|
return result;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
**Core Test Infrastructure:**
|
|
- **MPITestCore.hpp** - Framework-agnostic base class
|
|
- **MPITestBase.hpp** - Google Test adapter (full API documentation)
|
|
- **MPIStandaloneTest.hpp** - Standalone test adapter
|
|
- **MPIEnvironment.hpp** - MPI environment setup
|
|
- **MPIEnvironment.cpp** - Multi-node GPU assignment implementation
|
|
|
|
**Test Examples:**
|
|
- **transport/P2pMPITests.cpp** - P2P transport tests (demonstrate single-node validation)
|
|
- **transport/ShmMPITests.cpp** - Shared memory transport tests (demonstrate single-node validation)
|
|
- **transport/NetMPITests.cpp** - Network transport tests (demonstrate multi-node capable tests)
|