Files
TheRock/rocm-systems/projects/rccl/test/common/MPIHelpers.hpp
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

459 lines
15 KiB
C++

/*************************************************************************
* Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved.
*
* See LICENSE.txt for license information
************************************************************************/
/**
* @file MPIHelpers.hpp
* @brief Shared MPI utility functions for both GTest and standalone tests
*
* Provides common functionality for MPI test initialization, GPU setup,
* and per-rank logging that can be used by both GTest-based tests and
* standalone tests (performance benchmarks, etc.).
*/
#ifndef MPI_HELPERS_HPP
#define MPI_HELPERS_HPP
#ifdef MPI_TESTS_ENABLED
#include <array>
#include <atomic>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <optional>
#include <string>
#include <thread>
#include <type_traits>
/**
* @namespace MPIHelpers
* @brief Shared MPI utilities for test infrastructure
*/
namespace MPIHelpers
{
// Returns the MPI world rank as a string for diagnostic messages.
// Probes OpenMPI, MPICH/PMIx, and SLURM env vars in order; returns "?" if none set.
inline const char* getMpiRankStr()
{
for(const char* var : {"OMPI_COMM_WORLD_RANK", "PMI_RANK", "PMIX_RANK", "SLURM_PROCID"})
{
const char* val = std::getenv(var);
if(val)
return val;
}
return "?";
}
/**
* @struct MPIContext
* @brief MPI environment context information
*/
struct MPIContext
{
int world_rank; ///< MPI rank in MPI_COMM_WORLD
int world_size; ///< Total number of MPI processes
int thread_support; ///< MPI thread support level provided
};
/**
* @brief Initialize MPI with thread support
*
* Initializes MPI with MPI_THREAD_MULTIPLE support and returns context info.
*
* @param argc Pointer to argc from main()
* @param argv Pointer to argv from main()
* @return MPIContext with rank, size, and thread support info
*
* @note Must be called before any other MPI operations
* @note Automatically sets MPIEnvironment static variables
*/
MPIContext initializeMPI(int* argc, char*** argv);
/**
* @brief Setup GPU device for this MPI rank
*
* Assigns GPU device based on local rank (ranks on same node).
* Uses MPI_COMM_TYPE_SHARED to detect node topology and assigns
* GPUs in round-robin fashion.
*
* @param world_rank MPI rank in MPI_COMM_WORLD
*
* @note Handles multiple ranks per node automatically
* @note Uses hipSetDevice() to assign GPU
*/
void setupGPU(int world_rank);
/**
* @class FileDescriptor
* @brief RAII wrapper for POSIX file descriptors
*
* Automatically closes file descriptor on destruction.
* Move-only semantics prevent accidental duplication.
*/
class FileDescriptor
{
public:
explicit FileDescriptor(int fd = -1) noexcept;
~FileDescriptor();
// Move-only semantics
FileDescriptor(FileDescriptor&& other) noexcept;
FileDescriptor& operator=(FileDescriptor&& other) noexcept;
// Delete copy operations
FileDescriptor(const FileDescriptor&) = delete;
FileDescriptor& operator=(const FileDescriptor&) = delete;
[[nodiscard]] int get() const noexcept;
[[nodiscard]] bool is_valid() const noexcept;
int release() noexcept;
private:
int fd_;
};
/**
* @class TeeThread
* @brief Thread for duplicating output to console and log file
*
* Used by rank 0 when per-rank logging is enabled to send output
* to both console and log file simultaneously.
*/
class TeeThread
{
public:
TeeThread(int read_fd, int console_fd, int log_fd);
~TeeThread();
// Delete copy/move operations
TeeThread(const TeeThread&) = delete;
TeeThread& operator=(const TeeThread&) = delete;
TeeThread(TeeThread&&) = delete;
TeeThread& operator=(TeeThread&&) = delete;
private:
void tee_loop();
int read_fd_;
int console_fd_;
int log_fd_;
std::atomic<bool> running_;
std::thread thread_;
};
/**
* @struct RankLogConfig
* @brief Per-rank logging configuration and state
*
* Manages file descriptors and threads for per-rank logging when
* RCCL_MPI_LOG_ALL_RANKS=1 environment variable is set.
*/
struct RankLogConfig
{
std::optional<FileDescriptor> log_fd; ///< Log file descriptor
std::optional<FileDescriptor> saved_stdout; ///< Saved stdout for restoration
std::optional<FileDescriptor> saved_stderr; ///< Saved stderr for restoration
std::optional<FileDescriptor> pipe_read_fd; ///< Pipe read end (rank 0 only)
std::optional<FileDescriptor> pipe_write_fd; ///< Pipe write end (rank 0 only)
std::unique_ptr<TeeThread> tee_thread; ///< Tee thread (rank 0 only)
bool logging_enabled{false}; ///< Is per-rank logging enabled?
bool is_rank_zero{false}; ///< Is this rank 0?
};
/**
* @brief Setup per-rank logging if RCCL_MPI_LOG_ALL_RANKS=1
*
* Configures output redirection for MPI ranks:
* - Rank 0: Output to BOTH console AND log file (tee behavior)
* - Rank 1-N: Output redirected to rccl_test_rank_<N>.log
*
* If RCCL_MPI_LOG_ALL_RANKS is not set:
* - Rank 0: Normal console output
* - Rank 1-N: Output suppressed (redirected to /dev/null)
*
* @param rank MPI rank in MPI_COMM_WORLD
* @return Optional RankLogConfig if logging was configured, std::nullopt otherwise
*
* @note Call before any test output
* @note Must call restoreRankLogging() at end to cleanup
*/
std::optional<RankLogConfig> setupRankLogging(int rank);
/**
* @brief Restore original stdout/stderr after per-rank logging
*
* Cleans up per-rank logging configuration and restores original
* stdout/stderr file descriptors.
*
* @param config RankLogConfig to cleanup
*
* @note Safe to call multiple times
* @note Flushes pending output before restoration
*/
void restoreRankLogging(RankLogConfig& config);
/**
* @brief True when RCCL_MPI_LOG_ALL_RANKS=1 (per-rank log files are active)
*/
inline bool isPerRankLoggingEnabled()
{
const char* env = std::getenv("RCCL_MPI_LOG_ALL_RANKS");
return env != nullptr && std::string(env) == "1";
}
/**
* @brief Path to the per-rank log file used when RCCL_MPI_LOG_ALL_RANKS=1
*
* Matches the filename created by setupRankLogging(). The name embeds the
* --gtest_filter test label when it identifies a single test
* (rccl_test_<Label>_rank_<R>_pid<P>.log) so every rank's file can be matched
* to the test that produced it; otherwise it falls back to
* rccl_test_rank_<R>_pid<P>.log. The file is shared by all tests in the process
* unless callers use TestLogAssertionContext::isolate_new_output.
*/
std::string getRankLogFilePath(int rank);
/**
* @brief Read an entire file into a string (e.g. NCCL_DEBUG_FILE output)
*
* @return File contents, or empty string if the file could not be read
*/
std::string readTextFile(const std::string& path);
/**
* @brief Read the full contents of a rank log file
*
* Flushes stdout/stderr first so output redirected via per-rank logging (tee/pipe)
* reaches the file before reading.
*
* @return File contents, or empty string if the file could not be read
*/
std::string readRankLogFile(int rank);
/**
* @brief Byte length of an existing file, or 0 if missing or on error
*/
std::uintmax_t getFileSizeBytes(const std::string& path);
/**
* @struct TestLogAssertionOptions
* @brief Configure scoped log capture for MPI test assertions
*
* Two independent mechanisms can be enabled at once:
* - capture_nccl_debug_file: RAII set/restore NCCL_DEBUG_FILE so NCCL writes debug
* lines to a dedicated file (set before ncclComm* init). Works alongside global
* per-rank logging from main_mpi.cpp.
* - read_per_rank_stderr_log: after the action under test, read rccl_test_rank_<r>.log
* (requires RCCL_MPI_LOG_ALL_RANKS=1 for non-empty stderr capture on that rank).
*
* Use readNcclDebugLog() / readPerRankStderrLog() after the code that should emit logs.
* With isolate_new_output, only bytes appended after this object is constructed are
* returned (reduces bleed from earlier tests in the same process).
*/
struct TestLogAssertionOptions
{
int mpi_rank{0};
bool capture_nccl_debug_file{false};
/** If capture_nccl_debug_file and empty, auto-path is built from getLogBaseDir()
* (priority: RCCL_TEST_LOG_DIR env var → binary directory → /tmp):
* GTest binary : <logdir>/rccl_<Suite>.<Test>_rank<R>_pid<P>.log
* Standalone : <logdir>/rccl_rank<R>_pid<P>.log
* Path is derived from GTest current_test_info() at construction time. */
std::string nccl_debug_file_path;
/** When true, the log file is unlinked after a PASSING test.
* Failing tests always keep their log so it is available for post-mortem
* inspection. Set RCCL_KEEP_TEST_LOGS=1 to keep all logs regardless. */
bool unlink_auto_generated_nccl_path{true};
bool unlink_explicit_nccl_path{false};
bool read_per_rank_stderr_log{false};
bool isolate_new_output{true};
};
/**
* @class TestLogAssertionContext
* @brief RAII scope for NCCL_DEBUG_FILE and/or per-rank log reads
*
* Construct at the start of a TEST_F (before communicator init when using
* capture_nccl_debug_file). Destructor restores NCCL_DEBUG_FILE and optionally
* unlinks the NCCL temp file.
*/
class TestLogAssertionContext
{
public:
explicit TestLogAssertionContext(const TestLogAssertionOptions& opts);
~TestLogAssertionContext();
TestLogAssertionContext(const TestLogAssertionContext&) = delete;
TestLogAssertionContext& operator=(const TestLogAssertionContext&) = delete;
[[nodiscard]] bool capturesNcclDebugFile() const noexcept { return capture_nccl_; }
[[nodiscard]] bool readsPerRankStderrLog() const noexcept { return read_per_rank_; }
/** Path passed to NCCL via NCCL_DEBUG_FILE when capture_nccl_debug_file is set */
[[nodiscard]] const std::string& ncclDebugFilePath() const noexcept { return nccl_path_; }
/** Content from the NCCL debug file (slice if isolate_new_output was set) */
[[nodiscard]] std::string readNcclDebugLog() const;
/** Content from rccl_test_rank_<rank>.log after flush (slice if isolated) */
[[nodiscard]] std::string readPerRankStderrLog() const;
private:
TestLogAssertionOptions opts_;
std::string nccl_path_;
std::string test_label_; ///< "Suite/Test" or "pid_<P>" for standalone
std::string saved_nccl_debug_env_;
bool saved_nccl_debug_present_{false};
bool env_modified_{false};
bool auto_nccl_path_{false};
bool capture_nccl_{false};
bool read_per_rank_{false};
std::uintmax_t nccl_start_offset_{0};
std::uintmax_t per_rank_start_offset_{0};
};
/** Scoped NCCL_DEBUG_FILE only (NCCL_DEBUG=INFO typical) */
TestLogAssertionOptions makeNcclDebugFileAssertionOptions(int mpi_rank);
/** Read rccl_test_rank_<rank>.log only (RCCL_MPI_LOG_ALL_RANKS=1) */
TestLogAssertionOptions makePerRankStderrAssertionOptions(int mpi_rank);
/** Both: NCCL debug file + per-rank stderr log (either read may contain the line) */
TestLogAssertionOptions makeCombinedAssertionLogOptions(int mpi_rank);
/**
* @brief Return the effective value of a numeric NCCL env var (mirrors NCCL_PARAM semantics).
* @param name Env var name (e.g. "NCCL_CTA_POLICY").
* @param ncclDefault NCCL compiled-in default (returned when the var is unset or empty).
*
* Mirrors ncclLoadParam behaviour: uses base 0 so that "0x10" parses as 16, "010" as 8, etc.
* An empty string is treated as unset (returns ncclDefault), consistent with RCCL's param code.
*/
template<typename T>
inline T getEnvParam(const char* name, T ncclDefault)
{
static_assert(std::is_integral_v<T>, "getEnvParam: T must be an integer type");
const char* v = std::getenv(name);
if(!v || v[0] == '\0') return ncclDefault;
char* endptr = nullptr;
errno = 0;
if constexpr(std::is_unsigned_v<T>)
{
const unsigned long long val = std::strtoull(v, &endptr, 0);
if(errno == ERANGE || endptr == v) return ncclDefault;
return static_cast<T>(val);
}
else
{
const long long val = std::strtoll(v, &endptr, 0);
if(errno == ERANGE || endptr == v) return ncclDefault;
return static_cast<T>(val);
}
}
/** RAII scoped set/restore of a single env var; restores (or unsets) on destruction.
*
* Warns on stderr (via getMpiRankStr()) when overriding an existing value so the
* caller knows the in-test override is active. The original value is always restored
* on destruction regardless of test outcome.
*/
struct MpiEnvGuard
{
const char* name;
std::string saved;
bool had{false};
MpiEnvGuard(const char* n, const char* v) : name(n)
{
const char* prev = std::getenv(n);
had = (prev != nullptr);
if(had)
{
saved = prev;
if(saved != v)
{
#ifdef RCCL_TEST_CHECKS_HPP
TEST_INFO("MpiEnvGuard overriding %s: '%s' -> '%s' (will restore on scope exit)",
n, saved.c_str(), v);
#else
fprintf(stderr,
"[MpiEnvGuard rank %s] overriding %s: '%s' -> '%s' (will restore on scope exit)\n",
getMpiRankStr(), n, saved.c_str(), v);
#endif
}
}
::setenv(n, v, /*overwrite=*/1);
}
~MpiEnvGuard()
{
if(had)
::setenv(name, saved.c_str(), 1);
else
::unsetenv(name);
}
MpiEnvGuard(const MpiEnvGuard&) = delete;
MpiEnvGuard& operator=(const MpiEnvGuard&) = delete;
};
/** RAII scoped force-unset of a single env var; restores the previous value
* (or leaves it unset) on destruction.
*
* Use this when a test needs to guarantee an env var is absent for the scope
* (e.g. NCCL_DEBUG_SUBSYS, to exercise RCCL's default subsystem mask)
* regardless of whatever ambient value CI or a prior test may have left set.
*/
struct MpiEnvUnsetGuard
{
const char* name;
std::string saved;
bool had{false};
explicit MpiEnvUnsetGuard(const char* n) : name(n)
{
const char* prev = std::getenv(n);
had = (prev != nullptr);
if(had)
{
saved = prev;
}
::unsetenv(n);
}
~MpiEnvUnsetGuard()
{
if(had)
::setenv(name, saved.c_str(), 1);
else
::unsetenv(name);
}
MpiEnvUnsetGuard(const MpiEnvUnsetGuard&) = delete;
MpiEnvUnsetGuard& operator=(const MpiEnvUnsetGuard&) = delete;
};
/**
* @brief Force RCCL to re-read NCCL_DEBUG / NCCL_DEBUG_SUBSYS / NCCL_DEBUG_FILE on next log call.
*
* ncclDebugInit() lazily caches its parsed env state process-wide, so changing
* these env vars mid-process (e.g. between two TEST_F cases sharing a binary)
* has no effect until this is called. Wraps the same dlsym'd
* ncclResetDebugInit() used internally by TestLogAssertionContext for
* NCCL_DEBUG_FILE changes; safe no-op if the symbol cannot be resolved.
*/
void resetNcclDebugState();
} // namespace MPIHelpers
#endif // MPI_TESTS_ENABLED
#endif // MPI_HELPERS_HPP