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
819 lines
27 KiB
C++
819 lines
27 KiB
C++
/*************************************************************************
|
|
* Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved.
|
|
*
|
|
* See LICENSE.txt for license information
|
|
************************************************************************/
|
|
|
|
#include "MPIHelpers.hpp"
|
|
|
|
#ifdef MPI_TESTS_ENABLED
|
|
|
|
#include "MPITestCore.hpp"
|
|
#include "MPIEnvironment.hpp"
|
|
#include <cctype>
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <dlfcn.h>
|
|
#include <fcntl.h>
|
|
#include <fstream>
|
|
#include <hip/hip_runtime.h>
|
|
#include <iostream>
|
|
#include <mpi.h>
|
|
#include <sstream>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
// RCCL_MPIHelpers_HAS_GTEST is set by the build system (CMakeLists.txt) for
|
|
// targets that actually link against GTest, so this guard is guaranteed to
|
|
// match the link graph rather than relying on header presence alone.
|
|
#if defined(RCCL_MPIHelpers_HAS_GTEST)
|
|
#include <gtest/gtest.h>
|
|
#endif
|
|
|
|
// resetNcclDebugFile() calls ncclResetDebugInit() via dlsym so that we
|
|
// never reference the deprecated symbol by name at compile or link time.
|
|
//
|
|
// Why dlsym instead of a direct call:
|
|
// - ncclResetDebugInit is marked __attribute__((deprecated)) in nccl.h,
|
|
// so a direct call triggers -Wdeprecated-declarations and requires a
|
|
// #pragma GCC diagnostic suppression.
|
|
// - pncclResetDebugInit (the non-deprecated profiling wrapper) is NOT
|
|
// exported from librccl.so, so it cannot be linked against directly.
|
|
// - dlsym(RTLD_DEFAULT, ...) resolves the symbol from the already-loaded
|
|
// librccl.so at runtime — no pragma, no link-time dependency.
|
|
// - If RCCL removes the API in a future release the call becomes a no-op
|
|
// (the static fn pointer stays null) rather than a link or build error.
|
|
static void resetNcclDebugFile()
|
|
{
|
|
using NcclResetFn = void (*)();
|
|
static NcclResetFn fn = reinterpret_cast<NcclResetFn>(
|
|
::dlsym(RTLD_DEFAULT, "ncclResetDebugInit"));
|
|
if(fn)
|
|
fn();
|
|
}
|
|
|
|
namespace MPIHelpers
|
|
{
|
|
namespace
|
|
{
|
|
std::string sliceFromOffset(const std::string& full, std::uintmax_t off)
|
|
{
|
|
if(off >= full.size())
|
|
{
|
|
return {};
|
|
}
|
|
return full.substr(static_cast<std::size_t>(off));
|
|
}
|
|
|
|
// Determine the base directory for per-rank and NCCL debug log files.
|
|
//
|
|
// Priority:
|
|
// 1. RCCL_TEST_LOG_DIR env var — explicit override, useful in CI.
|
|
// 2. Directory of the test binary (/proc/self/exe) — ties logs to the
|
|
// specific build that produced them; different builds never share logs.
|
|
// Skipped if not writable (e.g. system-installed or read-only prefix).
|
|
// 3. /tmp — last-resort fallback.
|
|
std::string getLogBaseDir()
|
|
{
|
|
const char* env = std::getenv("RCCL_TEST_LOG_DIR");
|
|
if(env && env[0] != '\0')
|
|
return env;
|
|
|
|
char buf[4096];
|
|
const ssize_t len = ::readlink("/proc/self/exe", buf, sizeof(buf) - 1);
|
|
if(len > 0)
|
|
{
|
|
buf[len] = '\0';
|
|
char* slash = ::strrchr(buf, '/');
|
|
if(slash)
|
|
{
|
|
*slash = '\0';
|
|
if(::access(buf, W_OK) == 0)
|
|
return std::string{buf};
|
|
// Binary dir is not writable; fall through to /tmp.
|
|
}
|
|
}
|
|
|
|
return "/tmp";
|
|
}
|
|
|
|
// Replace every character that is not alphanumeric, '.', or '-' with '_'
|
|
// so that test-suite and test-case names are safe to embed in a file path.
|
|
std::string sanitizeForFilename(const std::string& s)
|
|
{
|
|
std::string out;
|
|
out.reserve(s.size());
|
|
for(unsigned char c : s)
|
|
{
|
|
out += (std::isalnum(c) || c == '.' || c == '-') ? static_cast<char>(c) : '_';
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Stable per-process label derived from the --gtest_filter argument.
|
|
//
|
|
// The per-rank stdout/stderr log file is opened once at process startup
|
|
// (before GTest knows which test is running) and is later looked up by name
|
|
// when reading it back, so the label must be constant for the whole process.
|
|
// The test runner launches one fully-qualified test per process, so the
|
|
// filter identifies that test and lets every rank's file be matched to it
|
|
// (e.g. `ls *<TestName>*`). Wildcard/list filters that don't map to a single
|
|
// test yield an empty label so the filename stays unchanged.
|
|
const std::string& getRunLabel()
|
|
{
|
|
static const std::string label = []() -> std::string {
|
|
std::string filter;
|
|
std::ifstream f("/proc/self/cmdline", std::ios::binary);
|
|
if(f)
|
|
{
|
|
const std::string key = "--gtest_filter=";
|
|
std::string arg;
|
|
char c;
|
|
while(f.get(c))
|
|
{
|
|
if(c == '\0')
|
|
{
|
|
if(arg.rfind(key, 0) == 0)
|
|
filter = arg.substr(key.size());
|
|
arg.clear();
|
|
}
|
|
else
|
|
{
|
|
arg += c;
|
|
}
|
|
}
|
|
}
|
|
if(filter.empty() || filter.find_first_of("*:?") != std::string::npos)
|
|
return {};
|
|
return sanitizeForFilename(filter);
|
|
}();
|
|
return label;
|
|
}
|
|
|
|
// Build a per-test, per-rank NCCL debug log path.
|
|
//
|
|
// Pattern: <logdir>/rccl_<TestSuite>.<TestName>_rank<R>_pid<P>.log
|
|
// PID suffix ensures each process invocation gets a unique file so that
|
|
// stale files from a different user or a previous run never block fopen("w").
|
|
std::string makeTestNameLogPath(int rank)
|
|
{
|
|
const auto pid = std::to_string(::getpid());
|
|
const std::string logdir = getLogBaseDir();
|
|
#if defined(RCCL_MPIHelpers_HAS_GTEST)
|
|
std::string suite;
|
|
std::string test;
|
|
|
|
const ::testing::TestInfo* info
|
|
= ::testing::UnitTest::GetInstance()->current_test_info();
|
|
if(info)
|
|
{
|
|
if(info->test_suite_name())
|
|
suite = info->test_suite_name();
|
|
if(info->name())
|
|
test = info->name();
|
|
}
|
|
|
|
if(!suite.empty() && !test.empty())
|
|
{
|
|
return logdir + "/rccl_" + sanitizeForFilename(suite) + "."
|
|
+ sanitizeForFilename(test) + "_rank" + std::to_string(rank)
|
|
+ "_pid" + pid + ".log";
|
|
}
|
|
#endif
|
|
return logdir + "/rccl_rank" + std::to_string(rank)
|
|
+ "_pid" + pid + ".log";
|
|
}
|
|
} // namespace
|
|
|
|
|
|
// ============================================================================
|
|
// FileDescriptor Implementation
|
|
// ============================================================================
|
|
|
|
FileDescriptor::FileDescriptor(int fd) noexcept : fd_(fd) {}
|
|
|
|
FileDescriptor::~FileDescriptor()
|
|
{
|
|
if(fd_ >= 0)
|
|
{
|
|
::close(fd_);
|
|
}
|
|
}
|
|
|
|
FileDescriptor::FileDescriptor(FileDescriptor&& other) noexcept : fd_(other.fd_)
|
|
{
|
|
other.fd_ = -1;
|
|
}
|
|
|
|
FileDescriptor& FileDescriptor::operator=(FileDescriptor&& other) noexcept
|
|
{
|
|
if(this != &other)
|
|
{
|
|
if(fd_ >= 0)
|
|
{
|
|
::close(fd_);
|
|
}
|
|
fd_ = other.fd_;
|
|
other.fd_ = -1;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
int FileDescriptor::get() const noexcept
|
|
{
|
|
return fd_;
|
|
}
|
|
|
|
bool FileDescriptor::is_valid() const noexcept
|
|
{
|
|
return fd_ >= 0;
|
|
}
|
|
|
|
int FileDescriptor::release() noexcept
|
|
{
|
|
const auto fd = fd_;
|
|
fd_ = -1;
|
|
return fd;
|
|
}
|
|
|
|
// ============================================================================
|
|
// TeeThread Implementation
|
|
// ============================================================================
|
|
|
|
TeeThread::TeeThread(int read_fd, int console_fd, int log_fd)
|
|
: read_fd_(read_fd), console_fd_(console_fd), log_fd_(log_fd), running_(true)
|
|
{
|
|
thread_ = std::thread([this]() { this->tee_loop(); });
|
|
}
|
|
|
|
TeeThread::~TeeThread()
|
|
{
|
|
running_ = false;
|
|
if(thread_.joinable())
|
|
{
|
|
thread_.join();
|
|
}
|
|
}
|
|
|
|
void TeeThread::tee_loop()
|
|
{
|
|
std::array<char, 4096> buffer;
|
|
while(running_)
|
|
{
|
|
const auto bytes_read = ::read(read_fd_, buffer.data(), buffer.size());
|
|
if(bytes_read <= 0)
|
|
{
|
|
if(bytes_read == 0 || errno != EINTR)
|
|
{
|
|
break; // EOF or error
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Write to console
|
|
[[maybe_unused]] auto console_written = ::write(console_fd_, buffer.data(), bytes_read);
|
|
|
|
// Write to log file
|
|
[[maybe_unused]] auto log_written = ::write(log_fd_, buffer.data(), bytes_read);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// MPI Initialization
|
|
// ============================================================================
|
|
|
|
MPIContext initializeMPI(int* argc, char*** argv)
|
|
{
|
|
MPIContext ctx;
|
|
|
|
// Initialize MPI with thread support
|
|
MPI_Init_thread(argc, argv, MPI_THREAD_MULTIPLE, &ctx.thread_support);
|
|
MPI_Comm_rank(MPI_COMM_WORLD, &ctx.world_rank);
|
|
MPI_Comm_size(MPI_COMM_WORLD, &ctx.world_size);
|
|
|
|
// Update global environment
|
|
MPIEnvironment::world_rank = ctx.world_rank;
|
|
MPIEnvironment::world_size = ctx.world_size;
|
|
MPIEnvironment::mpi_initialized = true;
|
|
|
|
return ctx;
|
|
}
|
|
|
|
// ============================================================================
|
|
// GPU Setup
|
|
// ============================================================================
|
|
|
|
void setupGPU(int world_rank)
|
|
{
|
|
int device_count = 0;
|
|
(void)hipGetDeviceCount(&device_count);
|
|
|
|
if(device_count > 0)
|
|
{
|
|
// Use MPI_COMM_TYPE_SHARED to detect local ranks on same node
|
|
MPI_Comm node_comm;
|
|
MPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, 0, MPI_INFO_NULL, &node_comm);
|
|
|
|
int local_rank, local_size;
|
|
MPI_Comm_rank(node_comm, &local_rank);
|
|
MPI_Comm_size(node_comm, &local_size);
|
|
|
|
// Cache multi-node detection result for isMultiNodeTest()
|
|
int world_size;
|
|
MPI_Comm_size(MPI_COMM_WORLD, &world_size);
|
|
MPIEnvironment::cached_multi_node_result = (local_size < world_size) ? 1 : 0;
|
|
|
|
// Assign GPU in round-robin fashion
|
|
int device_id = local_rank % device_count;
|
|
(void)hipSetDevice(device_id);
|
|
|
|
MPI_Comm_free(&node_comm);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Per-Rank Logging
|
|
// ============================================================================
|
|
|
|
std::optional<RankLogConfig> setupRankLogging(int rank)
|
|
{
|
|
const auto* env_value = std::getenv("RCCL_MPI_LOG_ALL_RANKS");
|
|
const bool per_rank_logging_enabled = (env_value && std::string(env_value) == "1");
|
|
|
|
RankLogConfig config;
|
|
config.logging_enabled = per_rank_logging_enabled;
|
|
config.is_rank_zero = (rank == 0);
|
|
|
|
// Non-zero ranks: Always redirect output (either to log file or /dev/null)
|
|
if(rank != 0)
|
|
{
|
|
// Save original stdout/stderr
|
|
config.saved_stdout = FileDescriptor{::dup(STDOUT_FILENO)};
|
|
config.saved_stderr = FileDescriptor{::dup(STDERR_FILENO)};
|
|
|
|
if(!config.saved_stdout->is_valid() || !config.saved_stderr->is_valid())
|
|
{
|
|
TEST_WARN("Rank %d: Failed to duplicate stdout/stderr", rank);
|
|
return std::nullopt;
|
|
}
|
|
|
|
if(per_rank_logging_enabled)
|
|
{
|
|
const auto log_filename = getRankLogFilePath(rank);
|
|
|
|
const auto log_fd = ::open(log_filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
|
|
if(log_fd < 0)
|
|
{
|
|
TEST_WARN("Rank %d: Failed to create log file: %s", rank, log_filename.c_str());
|
|
return std::nullopt;
|
|
}
|
|
|
|
config.log_fd = FileDescriptor{log_fd};
|
|
|
|
// Redirect stdout/stderr to log file
|
|
if(::dup2(log_fd, STDOUT_FILENO) < 0 || ::dup2(log_fd, STDERR_FILENO) < 0)
|
|
{
|
|
TEST_WARN("Rank %d: Failed to redirect to log file", rank);
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Debug: Write initial marker to log file (AFTER redirection)
|
|
TEST_INFO("===== LOG FILE FOR RANK %d =====", rank);
|
|
}
|
|
else
|
|
{
|
|
// Default: Suppress all output by redirecting to /dev/null
|
|
const auto null_fd = ::open("/dev/null", O_WRONLY);
|
|
if(null_fd < 0)
|
|
{
|
|
TEST_WARN("Rank %d: Failed to open /dev/null", rank);
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Redirect stdout/stderr to /dev/null
|
|
if(::dup2(null_fd, STDOUT_FILENO) < 0 || ::dup2(null_fd, STDERR_FILENO) < 0)
|
|
{
|
|
TEST_WARN("Rank %d: Failed to redirect to /dev/null", rank);
|
|
::close(null_fd);
|
|
return std::nullopt;
|
|
}
|
|
|
|
::close(null_fd);
|
|
}
|
|
|
|
// Disable buffering for immediate output
|
|
std::setvbuf(stdout, nullptr, _IONBF, 0);
|
|
std::setvbuf(stderr, nullptr, _IONBF, 0);
|
|
|
|
return config;
|
|
}
|
|
|
|
// Rank 0: Only redirect if per-rank logging is enabled (for tee functionality)
|
|
if(!per_rank_logging_enabled)
|
|
{
|
|
return std::nullopt; // Rank 0 outputs to console normally
|
|
}
|
|
|
|
// Create log file for rank 0
|
|
const auto log_filename = getRankLogFilePath(rank);
|
|
|
|
TEST_TRACE("Rank %d (rank 0 tee mode) opening log file: %s", rank, log_filename.c_str());
|
|
|
|
const auto log_fd = ::open(log_filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
|
|
if(log_fd < 0)
|
|
{
|
|
TEST_WARN("Rank %d: Failed to create log file: %s", rank, log_filename.c_str());
|
|
return std::nullopt;
|
|
}
|
|
|
|
config.log_fd = FileDescriptor{log_fd};
|
|
|
|
// Debug: Write initial marker directly to log file (BEFORE redirection)
|
|
const char* marker = "===== LOG FILE FOR RANK 0 (TEE MODE) =====\n";
|
|
[[maybe_unused]] auto written = ::write(log_fd, marker, std::strlen(marker));
|
|
|
|
// Rank 0 with per-rank logging: Output to BOTH console AND log file (tee behavior)
|
|
// Print banner before redirection
|
|
const std::string& run_label = getRunLabel();
|
|
const std::string file_glob = (run_label.empty() ? std::string{"rccl_test"}
|
|
: "rccl_test_" + run_label)
|
|
+ "_rank_*_pid*.log";
|
|
|
|
TEST_INFO("Per-Rank Logging ENABLED (RCCL_MPI_LOG_ALL_RANKS=1)");
|
|
TEST_INFO("Rank 0 : Output to BOTH console AND %s", log_filename.c_str());
|
|
TEST_INFO("Ranks 1-N : Output redirected to per-rank log files");
|
|
TEST_INFO("Rank files : %s (each rank has its own PID)", file_glob.c_str());
|
|
TEST_INFO("Log dir : %s (override: RCCL_TEST_LOG_DIR)", getLogBaseDir().c_str());
|
|
|
|
// Save original stdout/stderr for tee thread
|
|
config.saved_stdout = FileDescriptor{::dup(STDOUT_FILENO)};
|
|
config.saved_stderr = FileDescriptor{::dup(STDERR_FILENO)};
|
|
|
|
if(!config.saved_stdout->is_valid() || !config.saved_stderr->is_valid())
|
|
{
|
|
TEST_WARN("Rank %d: Failed to duplicate stdout/stderr", rank);
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Create pipes for tee functionality
|
|
int pipe_fds[2];
|
|
if(::pipe(pipe_fds) < 0)
|
|
{
|
|
TEST_WARN("Rank %d: Failed to create pipe", rank);
|
|
return std::nullopt;
|
|
}
|
|
|
|
config.pipe_read_fd = FileDescriptor{pipe_fds[0]};
|
|
config.pipe_write_fd = FileDescriptor{pipe_fds[1]};
|
|
|
|
// Start tee thread to duplicate output to both console and log file
|
|
try
|
|
{
|
|
config.tee_thread = std::make_unique<TeeThread>(config.pipe_read_fd->get(),
|
|
config.saved_stdout->get(),
|
|
log_fd);
|
|
}
|
|
catch(const std::exception& e)
|
|
{
|
|
TEST_WARN("Rank %d: Failed to start tee thread: %s", rank, e.what());
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Redirect stdout/stderr to the pipe write end
|
|
if(::dup2(config.pipe_write_fd->get(), STDOUT_FILENO) < 0
|
|
|| ::dup2(config.pipe_write_fd->get(), STDERR_FILENO) < 0)
|
|
{
|
|
TEST_WARN("Rank %d: Failed to redirect to pipe", rank);
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Disable buffering for immediate output
|
|
std::setvbuf(stdout, nullptr, _IONBF, 0);
|
|
std::setvbuf(stderr, nullptr, _IONBF, 0);
|
|
|
|
return config;
|
|
}
|
|
|
|
std::string getRankLogFilePath(int rank)
|
|
{
|
|
const std::string& label = getRunLabel();
|
|
const std::string prefix = label.empty() ? std::string{"rccl_test"} : "rccl_test_" + label;
|
|
return getLogBaseDir() + "/" + prefix + "_rank_" + std::to_string(rank)
|
|
+ "_pid" + std::to_string(::getpid()) + ".log";
|
|
}
|
|
|
|
std::uintmax_t getFileSizeBytes(const std::string& path)
|
|
{
|
|
struct stat st
|
|
{
|
|
};
|
|
if(::stat(path.c_str(), &st) != 0)
|
|
{
|
|
return 0;
|
|
}
|
|
return static_cast<std::uintmax_t>(st.st_size);
|
|
}
|
|
|
|
std::string readTextFile(const std::string& path)
|
|
{
|
|
std::ifstream file(path);
|
|
if(!file.is_open())
|
|
{
|
|
TEST_WARN("Could not open file: %s", path.c_str());
|
|
return "";
|
|
}
|
|
std::ostringstream ss;
|
|
ss << file.rdbuf();
|
|
return ss.str();
|
|
}
|
|
|
|
std::string readRankLogFile(int rank)
|
|
{
|
|
if(!isPerRankLoggingEnabled())
|
|
return {};
|
|
std::fflush(stdout);
|
|
std::fflush(stderr);
|
|
return readTextFile(getRankLogFilePath(rank));
|
|
}
|
|
|
|
TestLogAssertionOptions makeNcclDebugFileAssertionOptions(int mpi_rank)
|
|
{
|
|
TestLogAssertionOptions o;
|
|
o.mpi_rank = mpi_rank;
|
|
o.capture_nccl_debug_file = true;
|
|
o.read_per_rank_stderr_log = false;
|
|
// Test-name-based paths are unique per test, so there is no truncation risk,
|
|
// auto-unlink log files on pass, keep on failure is implemented in the destructor.
|
|
// Set RCCL_KEEP_TEST_LOGS=1 to retain all logs unconditionally.
|
|
o.unlink_auto_generated_nccl_path = true;
|
|
return o;
|
|
}
|
|
|
|
TestLogAssertionOptions makePerRankStderrAssertionOptions(int mpi_rank)
|
|
{
|
|
TestLogAssertionOptions o;
|
|
o.mpi_rank = mpi_rank;
|
|
o.capture_nccl_debug_file = false;
|
|
o.read_per_rank_stderr_log = true;
|
|
return o;
|
|
}
|
|
|
|
TestLogAssertionOptions makeCombinedAssertionLogOptions(int mpi_rank)
|
|
{
|
|
TestLogAssertionOptions o;
|
|
o.mpi_rank = mpi_rank;
|
|
o.capture_nccl_debug_file = true;
|
|
o.read_per_rank_stderr_log = true;
|
|
return o;
|
|
}
|
|
|
|
void resetNcclDebugState()
|
|
{
|
|
resetNcclDebugFile();
|
|
}
|
|
|
|
TestLogAssertionContext::TestLogAssertionContext(const TestLogAssertionOptions& opts)
|
|
: opts_(opts)
|
|
, capture_nccl_(opts.capture_nccl_debug_file)
|
|
, read_per_rank_(opts.read_per_rank_stderr_log)
|
|
{
|
|
const int rank = opts.mpi_rank;
|
|
|
|
// Capture the test label for use in the log-file header (written in the
|
|
// destructor). GTest tests get "Suite/TestName"; standalone binaries
|
|
// that don't link against GTest get "pid_<PID>" as a fallback so the
|
|
// log can still be correlated to the process that produced it.
|
|
#if defined(RCCL_MPIHelpers_HAS_GTEST)
|
|
{
|
|
const ::testing::TestInfo* info
|
|
= ::testing::UnitTest::GetInstance()->current_test_info();
|
|
if(info && info->test_suite_name() && info->name())
|
|
test_label_ = std::string{info->test_suite_name()} + "/" + info->name();
|
|
}
|
|
#endif
|
|
if(test_label_.empty())
|
|
test_label_ = std::string{"pid_"} + std::to_string(::getpid());
|
|
|
|
if(capture_nccl_)
|
|
{
|
|
if(opts_.nccl_debug_file_path.empty())
|
|
{
|
|
nccl_path_ = makeTestNameLogPath(rank);
|
|
auto_nccl_path_ = true;
|
|
}
|
|
else
|
|
{
|
|
nccl_path_ = opts_.nccl_debug_file_path;
|
|
auto_nccl_path_ = false;
|
|
}
|
|
|
|
const char* prev = std::getenv("NCCL_DEBUG_FILE");
|
|
if(prev != nullptr)
|
|
{
|
|
saved_nccl_debug_env_ = prev;
|
|
saved_nccl_debug_present_ = true;
|
|
}
|
|
if(::setenv("NCCL_DEBUG_FILE", nccl_path_.c_str(), 1) != 0)
|
|
{
|
|
TEST_WARN("Rank %d: setenv NCCL_DEBUG_FILE failed", rank);
|
|
}
|
|
else
|
|
{
|
|
env_modified_ = true;
|
|
// Tell NCCL to close its current log file and re-read NCCL_DEBUG_FILE
|
|
// on the next log output. Without this the global ncclDebugFile remains
|
|
// pointed at the previous test's (possibly already-unlinked) file handle.
|
|
resetNcclDebugFile();
|
|
}
|
|
|
|
// Remove any log file left over from a previous test run (e.g. a
|
|
// skipped run that kept its log). NCCL always opens the file with
|
|
// fopen("w") which truncates, so if we recorded the OLD file's size
|
|
// as nccl_start_offset_, readNcclDebugLog() would slice past all of
|
|
// the new content and return an empty string on every subsequent run.
|
|
// Only unlink auto-generated paths; do not clobber a user-supplied file.
|
|
if(auto_nccl_path_)
|
|
{
|
|
(void)::unlink(nccl_path_.c_str());
|
|
}
|
|
|
|
if(opts_.isolate_new_output)
|
|
{
|
|
// File was just unlinked; getFileSizeBytes always returns 0 here.
|
|
// The assignment is kept for symmetry and clarity.
|
|
nccl_start_offset_ = getFileSizeBytes(nccl_path_);
|
|
}
|
|
}
|
|
|
|
if(read_per_rank_)
|
|
{
|
|
if(opts_.isolate_new_output)
|
|
{
|
|
std::fflush(stdout);
|
|
std::fflush(stderr);
|
|
per_rank_start_offset_ = getFileSizeBytes(getRankLogFilePath(rank));
|
|
}
|
|
}
|
|
}
|
|
|
|
TestLogAssertionContext::~TestLogAssertionContext()
|
|
{
|
|
if(env_modified_)
|
|
{
|
|
if(saved_nccl_debug_present_)
|
|
{
|
|
(void)::setenv("NCCL_DEBUG_FILE", saved_nccl_debug_env_.c_str(), 1);
|
|
}
|
|
else
|
|
{
|
|
(void)::unsetenv("NCCL_DEBUG_FILE");
|
|
}
|
|
// Flush all stdio streams (including NCCL's internal FILE* buffer) before
|
|
// reading the log file. resetNcclDebugFile() arms the lazy close but does
|
|
// not flush or close the handle immediately; fflush(nullptr) ensures every
|
|
// pending byte has been written to disk before we read and rewrite the file.
|
|
std::fflush(nullptr);
|
|
resetNcclDebugFile();
|
|
}
|
|
|
|
if(capture_nccl_ && !nccl_path_.empty())
|
|
{
|
|
// ---------------------------------------------------------------
|
|
// Decide whether this log will be kept or deleted BEFORE doing
|
|
// any file I/O: large NCCL_DEBUG=INFO logs on passing tests would
|
|
// otherwise add avoidable teardown I/O and rank skew if we read
|
|
// and rewrite a file we are about to delete.
|
|
// ---------------------------------------------------------------
|
|
|
|
// Treat RCCL_KEEP_TEST_LOGS=0 (or empty) as "don't keep"; any other
|
|
// non-empty value (typically "1") activates unconditional retention.
|
|
const char* keep_env = std::getenv("RCCL_KEEP_TEST_LOGS");
|
|
const bool keep_all = keep_env && keep_env[0] != '\0' && keep_env[0] != '0';
|
|
|
|
bool test_passed = false; // conservative default: keep when uncertain
|
|
#if defined(RCCL_MPIHelpers_HAS_GTEST)
|
|
{
|
|
const ::testing::TestInfo* info
|
|
= ::testing::UnitTest::GetInstance()->current_test_info();
|
|
if(info && info->result())
|
|
test_passed = info->result()->Passed();
|
|
}
|
|
#endif
|
|
|
|
const bool unlink_it
|
|
= !keep_all
|
|
&& test_passed
|
|
&& ((auto_nccl_path_ && opts_.unlink_auto_generated_nccl_path)
|
|
|| (!auto_nccl_path_ && opts_.unlink_explicit_nccl_path));
|
|
|
|
if(unlink_it)
|
|
{
|
|
(void)::unlink(nccl_path_.c_str());
|
|
}
|
|
else
|
|
{
|
|
// ---------------------------------------------------------------
|
|
// Prepend a one-line header to the log file so it is
|
|
// self-identifying regardless of whether the name is human-readable
|
|
// (GTest path) or opaque (pid-based standalone path).
|
|
//
|
|
// The header is written here (destructor) rather than in the
|
|
// constructor because NCCL opens the file with fopen("w") on its
|
|
// first lazy-init log call, which would truncate anything written
|
|
// earlier. By writing in the destructor — after the communicator
|
|
// has already been torn down and fflush(nullptr) has drained NCCL's
|
|
// internal buffer — the file is complete and safe to rewrite.
|
|
// ---------------------------------------------------------------
|
|
const std::string content = readTextFile(nccl_path_);
|
|
if(!content.empty())
|
|
{
|
|
std::ostringstream header;
|
|
header << "=== RCCL Test Log"
|
|
<< " | Test: " << test_label_
|
|
<< " | Rank: " << opts_.mpi_rank
|
|
<< " | PID: " << ::getpid()
|
|
<< " ===\n";
|
|
const std::string hdr = header.str();
|
|
|
|
if(FILE* f = std::fopen(nccl_path_.c_str(), "w"))
|
|
{
|
|
std::fwrite(hdr.c_str(), 1, hdr.size(), f);
|
|
std::fwrite(content.c_str(), 1, content.size(), f);
|
|
std::fclose(f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string TestLogAssertionContext::readNcclDebugLog() const
|
|
{
|
|
if(!capture_nccl_)
|
|
{
|
|
return {};
|
|
}
|
|
// NCCL writes to a stdio FILE* with block-buffering. Flush all open
|
|
// streams so that any bytes still sitting in the libc buffer (e.g.
|
|
// "Init CE" / "CE: rank" lines written during the most recent collective)
|
|
// are committed to disk before we read the file back.
|
|
std::fflush(nullptr);
|
|
const std::string full = readTextFile(nccl_path_);
|
|
return opts_.isolate_new_output ? sliceFromOffset(full, nccl_start_offset_) : full;
|
|
}
|
|
|
|
std::string TestLogAssertionContext::readPerRankStderrLog() const
|
|
{
|
|
if(!read_per_rank_ || !isPerRankLoggingEnabled())
|
|
{
|
|
return {};
|
|
}
|
|
std::fflush(stdout);
|
|
std::fflush(stderr);
|
|
const std::string full = readTextFile(getRankLogFilePath(opts_.mpi_rank));
|
|
return opts_.isolate_new_output ? sliceFromOffset(full, per_rank_start_offset_) : full;
|
|
}
|
|
|
|
void restoreRankLogging(RankLogConfig& config)
|
|
{
|
|
// Only restore if we actually redirected (have saved stdout/stderr)
|
|
if(!config.saved_stdout || !config.saved_stdout->is_valid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Flush any pending output
|
|
std::fflush(stdout);
|
|
std::fflush(stderr);
|
|
|
|
// CRITICAL: Restore stdout/stderr BEFORE closing pipe
|
|
// The tee thread won't get EOF until ALL write ends are closed
|
|
if(config.saved_stdout && config.saved_stdout->is_valid())
|
|
{
|
|
::dup2(config.saved_stdout->get(), STDOUT_FILENO);
|
|
}
|
|
|
|
if(config.saved_stderr && config.saved_stderr->is_valid())
|
|
{
|
|
::dup2(config.saved_stderr->get(), STDERR_FILENO);
|
|
}
|
|
|
|
if(config.is_rank_zero && config.tee_thread)
|
|
{
|
|
// For rank 0 with per-rank logging: Stop the tee thread
|
|
// Close the pipe write end to signal EOF to the tee thread
|
|
config.pipe_write_fd.reset();
|
|
|
|
// Wait for tee thread to finish processing
|
|
config.tee_thread.reset();
|
|
|
|
// Close pipe read end
|
|
config.pipe_read_fd.reset();
|
|
}
|
|
}
|
|
|
|
} // namespace MPIHelpers
|
|
|
|
#endif // MPI_TESTS_ENABLED
|