Files
nvidia--tensorrt/samples/sampleDistCollective/sampleDistCollective.cpp
T
wehub-resource-sync c8a779b1bb
Docker Image CI / build-ubuntu2004 (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:36:55 +08:00

902 lines
35 KiB
C++

/*
* SPDX-FileCopyrightText: Copyright (c) 1993-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <algorithm>
#include <cassert>
#include <cctype>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <unistd.h>
#include <vector>
#include "NvInfer.h"
#include "NvInferRuntime.h"
#include "common.h"
#include "logger.h"
#include "sampleDevice.h"
#include "sampleEngines.h"
#include "sampleInference.h"
#include "sampleOptions.h"
#include "sampleReporting.h"
using namespace nvinfer1;
using namespace sample;
using namespace samplesCommon;
// MD code start
#include <nccl.h>
// MD code end
using namespace std;
//! Checks NCCL return codes and asserts on failure since NCCL errors are unrecoverable communication failures.
#define NCCLCHECK(cmd) \
do \
{ \
ncclResult_t const r = (cmd); \
ASSERT(r == ncclSuccess); \
} while (0)
#define CHECK_CUDA(status) \
if (status != cudaSuccess) \
{ \
fprintf(stderr, "CUDA Error: %s\n", cudaGetErrorString(status)); \
exit(EXIT_FAILURE); \
}
namespace
{
using LibraryPtr = std::unique_ptr<DynamicLibrary>;
#if !TRT_STATIC
#if defined(_WIN32)
std::string const kNVINFER_PLUGIN_LIBNAME
= std::string{"nvinfer_plugin_"} + std::to_string(NV_TENSORRT_MAJOR) + std::string{".dll"};
std::string const kNVINFER_LIBNAME = std::string{"nvinfer_"} + std::to_string(NV_TENSORRT_MAJOR) + std::string{".dll"};
std::string const kNVONNXPARSER_LIBNAME
= std::string{"nvonnxparser_"} + std::to_string(NV_TENSORRT_MAJOR) + std::string{".dll"};
std::string const kNVINFER_LEAN_LIBNAME
= std::string{"nvinfer_lean_"} + std::to_string(NV_TENSORRT_MAJOR) + std::string{".dll"};
std::string const kNVINFER_DISPATCH_LIBNAME
= std::string{"nvinfer_dispatch_"} + std::to_string(NV_TENSORRT_MAJOR) + std::string{".dll"};
#else
std::string const kNVINFER_PLUGIN_LIBNAME = std::string{"libnvinfer_plugin.so."} + std::to_string(NV_TENSORRT_MAJOR);
std::string const kNVINFER_LIBNAME = std::string{"libnvinfer.so."} + std::to_string(NV_TENSORRT_MAJOR);
std::string const kNVONNXPARSER_LIBNAME = std::string{"libnvonnxparser.so."} + std::to_string(NV_TENSORRT_MAJOR);
std::string const kNVINFER_LEAN_LIBNAME = std::string{"libnvinfer_lean.so."} + std::to_string(NV_TENSORRT_MAJOR);
std::string const kNVINFER_DISPATCH_LIBNAME
= std::string{"libnvinfer_dispatch.so."} + std::to_string(NV_TENSORRT_MAJOR);
#endif
#endif // !TRT_STATIC
std::function<void*(void*, int32_t)> pCreateInferRuntimeInternal{};
std::function<void*(void*, void*, int32_t)> pCreateInferRefitterInternal{};
std::function<void*(void*, int32_t)> pCreateInferBuilderInternal{};
std::function<void*(void*, void*, int)> pCreateNvOnnxParserInternal{};
std::function<void*(void*, void*, int)> pCreateNvOnnxRefitterInternal{};
//! Track runtime used for the execution of trtexec.
//! Must be tracked as a global variable due to how library init functions APIs are organized.
RuntimeMode gUseRuntime = RuntimeMode::kFULL;
#if !TRT_STATIC
template <typename FetchPtrs>
bool initLibrary(LibraryPtr& libPtr, std::string const& libName, FetchPtrs fetchFunc)
{
if (libPtr != nullptr)
{
return true;
}
try
{
libPtr.reset(new DynamicLibrary{libName});
fetchFunc(libPtr.get());
}
catch (std::exception const& e)
{
libPtr.reset();
sample::gLogError << "Could not load library " << libName << ": " << e.what() << std::endl;
return false;
}
catch (...)
{
libPtr.reset();
sample::gLogError << "Could not load library " << libName << std::endl;
return false;
}
return true;
}
#endif // !TRT_STATIC
bool initNvinfer()
{
#if !TRT_STATIC
static LibraryPtr libnvinferPtr{};
auto fetchPtrs = [](DynamicLibrary* l) {
pCreateInferRuntimeInternal = l->symbolAddress<void*(void*, int32_t)>("createInferRuntime_INTERNAL");
try
{
pCreateInferRefitterInternal
= l->symbolAddress<void*(void*, void*, int32_t)>("createInferRefitter_INTERNAL");
}
catch (const std::exception& e)
{
sample::gLogWarning << "Could not load function createInferRefitter_INTERNAL : " << e.what() << std::endl;
}
if (gUseRuntime == RuntimeMode::kFULL)
{
pCreateInferBuilderInternal = l->symbolAddress<void*(void*, int32_t)>("createInferBuilder_INTERNAL");
}
};
return initLibrary(libnvinferPtr, sample::getRuntimeLibraryName(gUseRuntime), fetchPtrs);
#else
pCreateInferRuntimeInternal = createInferRuntime_INTERNAL;
pCreateInferRefitterInternal = createInferRefitter_INTERNAL;
pCreateInferBuilderInternal = createInferBuilder_INTERNAL;
return true;
#endif // !TRT_STATIC
}
bool initNvonnxparser()
{
#if !TRT_STATIC
static LibraryPtr libnvonnxparserPtr{};
auto fetchPtrs = [](DynamicLibrary* l) {
pCreateNvOnnxParserInternal = l->symbolAddress<void*(void*, void*, int)>("createNvOnnxParser_INTERNAL");
pCreateNvOnnxRefitterInternal = l->symbolAddress<void*(void*, void*, int)>("createNvOnnxParserRefitter_INTERNAL");
};
return initLibrary(libnvonnxparserPtr, kNVONNXPARSER_LIBNAME, fetchPtrs);
#else
pCreateNvOnnxParserInternal = createNvOnnxParser_INTERNAL;
pCreateNvOnnxRefitterInternal = createNvOnnxParserRefitter_INTERNAL;
return true;
#endif // !TRT_STATIC
}
[[nodiscard]] std::string toString(CollectiveOperation op)
{
switch (op)
{
case CollectiveOperation::kALL_REDUCE: return "ALL_REDUCE";
case CollectiveOperation::kALL_GATHER: return "ALL_GATHER";
case CollectiveOperation::kBROADCAST: return "BROADCAST";
case CollectiveOperation::kREDUCE: return "REDUCE";
case CollectiveOperation::kREDUCE_SCATTER: return "REDUCE_SCATTER";
case CollectiveOperation::kALL_TO_ALL: return "ALL_TO_ALL";
case CollectiveOperation::kGATHER: return "GATHER";
case CollectiveOperation::kSCATTER: return "SCATTER";
}
throw std::runtime_error("Unknown CollectiveOperation");
}
[[nodiscard]] bool icharEquals(char a, char b)
{
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
}
//! Case-insensitive string equality:
[[nodiscard]] bool iequals(std::string_view lhs, std::string_view rhs)
{
return std::equal(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), icharEquals);
}
//! Parse operation string to CollectiveOperation enum
[[nodiscard]] std::optional<CollectiveOperation> parseCollectiveOp(std::string_view opStr)
{
if (iequals(opStr, "all_reduce"))
{
return CollectiveOperation::kALL_REDUCE;
}
if (iequals(opStr, "all_gather"))
{
return CollectiveOperation::kALL_GATHER;
}
if (iequals(opStr, "broadcast"))
{
return CollectiveOperation::kBROADCAST;
}
if (iequals(opStr, "reduce"))
{
return CollectiveOperation::kREDUCE;
}
if (iequals(opStr, "reduce_scatter"))
{
return CollectiveOperation::kREDUCE_SCATTER;
}
if (iequals(opStr, "all_to_all"))
{
return CollectiveOperation::kALL_TO_ALL;
}
if (iequals(opStr, "gather"))
{
return CollectiveOperation::kGATHER;
}
if (iequals(opStr, "scatter"))
{
return CollectiveOperation::kSCATTER;
}
return std::nullopt;
}
void printUsage(char const* programName)
{
std::cout << "Usage:" << std::endl;
std::cout << " Set TRT_MY_RANK, TRT_WORLD_SIZE, and TRT_NCCL_ID_FILE, then run " << programName
<< " --op <operation>" << std::endl;
std::cout << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " --op <operation> Specify the collective operation to test (required)." << std::endl;
std::cout << " Valid operations: all_reduce, all_gather, broadcast, reduce, reduce_scatter, all_to_all, gather, scatter";
std::cout << std::endl;
std::cout << " --help, -h Show this help message." << std::endl;
std::cout << std::endl;
std::cout << "Environment Variables (required):" << std::endl;
std::cout << " TRT_MY_RANK The rank of this process (0 to WORLD_SIZE-1)." << std::endl;
std::cout << " TRT_WORLD_SIZE The total number of processes." << std::endl;
std::cout << " TRT_NCCL_ID_FILE Path to a shared file for NCCL ID coordination." << std::endl;
std::cout << " Rank 0 writes the NCCL ID to this file, other ranks read from it." << std::endl;
std::cout << " The file should be empty or non-existent before starting." << std::endl;
std::cout << std::endl;
std::cout << "Example commands:" << std::endl;
std::cout << " SLURM:" << std::endl;
std::cout << " srun --ntasks=2 bash -lc 'export TRT_MY_RANK=$SLURM_PROCID; \\" << std::endl;
std::cout << " export TRT_WORLD_SIZE=$SLURM_NTASKS; \\" << std::endl;
std::cout << " export TRT_NCCL_ID_FILE=/tmp/nccl_id.txt; \\" << std::endl;
std::cout << " " << programName << " --op all_reduce'" << std::endl;
std::cout << std::endl;
std::cout << " Open MPI:" << std::endl;
std::cout << " mpirun -np 2 bash -lc 'export TRT_MY_RANK=$OMPI_COMM_WORLD_RANK; \\" << std::endl;
std::cout << " export TRT_WORLD_SIZE=$OMPI_COMM_WORLD_SIZE; \\" << std::endl;
std::cout << " export TRT_NCCL_ID_FILE=/tmp/nccl_id.txt; \\" << std::endl;
std::cout << " " << programName << " --op all_reduce'" << std::endl;
}
//! Get rank from TRT_MY_RANK environment variable.
//! Users should set this variable via a launcher wrapper script.
[[nodiscard]] int32_t getRankFromEnv()
{
char const* rankStr = std::getenv("TRT_MY_RANK");
if (!rankStr)
{
sample::gLogError << "FATAL: TRT_MY_RANK environment variable is not set!" << std::endl;
sample::gLogError << "Please set TRT_MY_RANK to the rank of this process (0 to WORLD_SIZE-1)." << std::endl;
sample::gLogError << "Run with --help for example commands." << std::endl;
ASSERT(false && "TRT_MY_RANK environment variable must be set");
}
return std::stoi(rankStr);
}
//! Get world size from TRT_WORLD_SIZE environment variable.
//! Users should set this variable via a launcher wrapper script.
[[nodiscard]] int32_t getWorldSizeFromEnv()
{
char const* worldSizeStr = std::getenv("TRT_WORLD_SIZE");
if (!worldSizeStr)
{
sample::gLogError << "FATAL: TRT_WORLD_SIZE environment variable is not set!" << std::endl;
sample::gLogError << "Please set TRT_WORLD_SIZE to the total number of processes." << std::endl;
sample::gLogError << "Run with --help for example commands." << std::endl;
ASSERT(false && "TRT_WORLD_SIZE environment variable must be set");
}
return std::stoi(worldSizeStr);
}
//! Convert a hex character to its integer value
[[nodiscard]] int32_t hexCharToInt(char c)
{
if (c >= '0' && c <= '9')
{
return c - '0';
}
if (c >= 'a' && c <= 'f')
{
return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F')
{
return c - 'A' + 10;
}
return -1;
}
//! Convert NCCL unique ID bytes to hex string
[[nodiscard]] std::string ncclIdToHex(ncclUniqueId const& id)
{
constexpr char kHEX_CHARS[] = "0123456789abcdef";
std::string hexStr;
hexStr.reserve(sizeof(ncclUniqueId) * 2);
for (size_t i = 0; i < sizeof(ncclUniqueId); ++i)
{
auto const byte = static_cast<uint8_t>(id.internal[i]);
hexStr += kHEX_CHARS[byte >> 4];
hexStr += kHEX_CHARS[byte & 0x0F];
}
return hexStr;
}
//! Parse hex string to NCCL unique ID
[[nodiscard]] ncclUniqueId hexToNcclId(std::string const& hexStr)
{
constexpr size_t kNCCL_UNIQUE_ID_BYTES = sizeof(ncclUniqueId);
constexpr size_t kEXPECTED_HEX_LEN = kNCCL_UNIQUE_ID_BYTES * 2;
if (hexStr.length() != kEXPECTED_HEX_LEN)
{
throw std::runtime_error("NCCL ID hex string has invalid length: " + std::to_string(hexStr.length())
+ " (expected " + std::to_string(kEXPECTED_HEX_LEN) + ")");
}
ncclUniqueId id;
for (size_t i = 0; i < kNCCL_UNIQUE_ID_BYTES; ++i)
{
int32_t const high = hexCharToInt(hexStr[2 * i]);
int32_t const low = hexCharToInt(hexStr[2 * i + 1]);
if (high < 0 || low < 0)
{
throw std::runtime_error(
"NCCL ID hex string contains invalid character at position " + std::to_string(2 * i));
}
id.internal[i] = static_cast<char>((high << 4) | low);
}
return id;
}
//! Get the NCCL ID file path from TRT_NCCL_ID_FILE environment variable.
[[nodiscard]] std::string getNcclIdFilePath()
{
char const* filePath = std::getenv("TRT_NCCL_ID_FILE");
if (!filePath)
{
sample::gLogError << "FATAL: TRT_NCCL_ID_FILE environment variable is not set!" << std::endl;
sample::gLogError << "Please set TRT_NCCL_ID_FILE to a shared file path accessible by all ranks." << std::endl;
sample::gLogError << "Run with --help for example commands." << std::endl;
ASSERT(false && "TRT_NCCL_ID_FILE environment variable must be set");
}
return std::string(filePath);
}
//! Get NCCL unique ID using file-based coordination.
//! Rank 0 generates the ID and writes it to the file.
//! Other ranks wait for the file to be written and read the ID from it.
[[nodiscard]] ncclUniqueId getNcclIdViaFile(int32_t rank)
{
std::string const filePath = getNcclIdFilePath();
constexpr size_t kEXPECTED_HEX_LEN = sizeof(ncclUniqueId) * 2;
constexpr int32_t kPOLL_INTERVAL_MS = 10;
constexpr int32_t kTIMEOUT_MS = 30000; // 30 seconds timeout
if (rank == 0)
{
// Rank 0: Check if stale file exists from a previous run
std::ifstream checkFile(filePath);
if (checkFile)
{
std::string content;
std::getline(checkFile, content);
if (!content.empty())
{
throw std::runtime_error(
"NCCL ID file already exists with content: " + filePath + "\n"
"This may be stale data from a previous run. Please delete it first:\n"
" rm -f " + filePath);
}
}
// Generate NCCL ID and write to file
ncclUniqueId id;
NCCLCHECK(ncclGetUniqueId(&id));
std::string const hexStr = ncclIdToHex(id);
std::ofstream outFile(filePath, std::ios::trunc);
if (!outFile)
{
throw std::runtime_error("Failed to open NCCL ID file for writing: " + filePath);
}
outFile << hexStr;
outFile.close();
sample::gLogInfo << "Rank 0 - Generated NCCL ID and wrote to file: " << filePath << std::endl;
return id;
}
else
{
// Other ranks: Wait for file to be written and read the ID
int32_t elapsedMs = 0;
std::string hexStr;
while (elapsedMs < kTIMEOUT_MS)
{
std::ifstream inFile(filePath);
if (inFile)
{
std::getline(inFile, hexStr);
if (hexStr.length() == kEXPECTED_HEX_LEN)
{
sample::gLogInfo << "Rank " << rank << " - Read NCCL ID from file: " << filePath << std::endl;
return hexToNcclId(hexStr);
}
}
// File not ready yet, wait and retry
std::this_thread::sleep_for(std::chrono::milliseconds(kPOLL_INTERVAL_MS));
elapsedMs += kPOLL_INTERVAL_MS;
}
throw std::runtime_error("Timeout waiting for NCCL ID file to be written by rank 0");
}
}
} // namespace
IRuntime* createRuntime()
{
if (!initNvinfer())
{
return {};
}
ASSERT(pCreateInferRuntimeInternal != nullptr);
return static_cast<IRuntime*>(pCreateInferRuntimeInternal(&gLogger.getTRTLogger(), NV_TENSORRT_VERSION));
}
IBuilder* createBuilder()
{
if (!initNvinfer())
{
return {};
}
ASSERT(pCreateInferBuilderInternal != nullptr);
return static_cast<IBuilder*>(pCreateInferBuilderInternal(&gLogger.getTRTLogger(), NV_TENSORRT_VERSION));
}
IRefitter* createRefitter(ICudaEngine& engine)
{
if (!initNvinfer())
{
return {};
}
ASSERT(pCreateInferRefitterInternal != nullptr);
return static_cast<IRefitter*>(pCreateInferRefitterInternal(&engine, &gLogger.getTRTLogger(), NV_TENSORRT_VERSION));
}
nvonnxparser::IParser* createONNXParser(INetworkDefinition& network)
{
if (!initNvonnxparser())
{
return {};
}
ASSERT(pCreateNvOnnxParserInternal != nullptr);
return static_cast<nvonnxparser::IParser*>(
pCreateNvOnnxParserInternal(&network, &gLogger.getTRTLogger(), NV_ONNX_PARSER_VERSION));
}
nvonnxparser::IParserRefitter* createONNXRefitter(IRefitter& refitter)
{
if (!initNvonnxparser())
{
return {};
}
ASSERT(pCreateNvOnnxRefitterInternal != nullptr);
return static_cast<nvonnxparser::IParserRefitter*>(
pCreateNvOnnxRefitterInternal(&refitter, &gLogger.getTRTLogger(), NV_ONNX_PARSER_VERSION));
}
//! Helper struct to hold test configuration for each collective operation
struct CollectiveTestConfig
{
CollectiveOperation op;
std::vector<float> rank0Input;
std::vector<float> rank1Input;
std::vector<float> rank0ExpectedOutput;
std::vector<float> rank1ExpectedOutput; // Different from rank0 for REDUCE_SCATTER
int32_t outputElementCount; // Number of output elements per rank
};
//! Get test configuration for a specific collective operation
CollectiveTestConfig getTestConfig(CollectiveOperation op, int32_t worldSize)
{
// Input: 12 elements per rank [3, 4]
// After transpose: [4, 3]
constexpr int32_t kINPUT_SIZE = 12;
switch (op)
{
case CollectiveOperation::kALL_GATHER:
{
// ALL_GATHER: Each rank contributes data, all ranks receive concatenated result
// Output: 12 * worldSize = 24 elements
std::vector<float> const expected = {0.0F, 1.0F, 2.0F, 3.0F, 100.0F, 101.0F, 102.0F, 103.0F, 4.0F, 5.0F, 6.0F,
7.0F, 104.0F, 105.0F, 106.0F, 107.0F, 8.0F, 9.0F, 10.0F, 11.0F, 108.0F, 109.0F, 110.0F, 111.0F};
return {CollectiveOperation::kALL_GATHER,
{0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F, 10.0F, 11.0F},
{100.0F, 101.0F, 102.0F, 103.0F, 104.0F, 105.0F, 106.0F, 107.0F, 108.0F, 109.0F, 110.0F, 111.0F}, expected,
expected, // Both ranks get same result
kINPUT_SIZE * worldSize};
}
case CollectiveOperation::kALL_REDUCE:
{
// ALL_REDUCE: Sum across all ranks, all ranks receive same result
std::vector<float> const expected
= {11.0F, 22.0F, 33.0F, 44.0F, 55.0F, 66.0F, 77.0F, 88.0F, 99.0F, 110.0F, 121.0F, 132.0F};
return {CollectiveOperation::kALL_REDUCE,
{1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F, 10.0F, 11.0F, 12.0F},
{10.0F, 20.0F, 30.0F, 40.0F, 50.0F, 60.0F, 70.0F, 80.0F, 90.0F, 100.0F, 110.0F, 120.0F}, expected,
expected, // Both ranks get same result
kINPUT_SIZE};
}
case CollectiveOperation::kBROADCAST:
{
// BROADCAST: Rank 0 sends data to all ranks
std::vector<float> const expected = {1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F, 10.0F, 11.0F, 12.0F};
return {CollectiveOperation::kBROADCAST,
{1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F, 10.0F, 11.0F, 12.0F},
{99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F}, expected,
expected, // Both ranks get same result
kINPUT_SIZE};
}
case CollectiveOperation::kREDUCE:
// REDUCE: Sum across all ranks, only root (rank 0) receives result
// rank1's output is undefined, use empty vector
return {CollectiveOperation::kREDUCE,
{1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F, 10.0F, 11.0F, 12.0F},
{10.0F, 20.0F, 30.0F, 40.0F, 50.0F, 60.0F, 70.0F, 80.0F, 90.0F, 100.0F, 110.0F, 120.0F},
{11.0F, 22.0F, 33.0F, 44.0F, 55.0F, 66.0F, 77.0F, 88.0F, 99.0F, 110.0F, 121.0F, 132.0F},
{}, // rank1's output is undefined
kINPUT_SIZE};
case CollectiveOperation::kREDUCE_SCATTER:
// REDUCE_SCATTER: Reduce then scatter - each rank gets a different portion
// Input [3,4] -> transpose [4,3] -> reduce_scatter [2,3] -> transpose [3,2] = 6 elements
// After reduce: [[11,55,99],[22,66,110],[33,77,121],[44,88,132]]
// rank0 gets first half [[11,55,99],[22,66,110]], after transpose: [[11,22],[55,66],[99,110]]
// rank1 gets second half [[33,77,121],[44,88,132]], after transpose: [[33,44],[77,88],[121,132]]
return {CollectiveOperation::kREDUCE_SCATTER,
{1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F, 10.0F, 11.0F, 12.0F},
{10.0F, 20.0F, 30.0F, 40.0F, 50.0F, 60.0F, 70.0F, 80.0F, 90.0F, 100.0F, 110.0F, 120.0F},
{11.0F, 22.0F, 55.0F, 66.0F, 99.0F, 110.0F}, // rank0 expected
{33.0F, 44.0F, 77.0F, 88.0F, 121.0F, 132.0F}, // rank1 expected
kINPUT_SIZE / worldSize};
case CollectiveOperation::kALL_TO_ALL:
{
// ALL_TO_ALL: Input [3,4] -> transpose [4,3] -> all_to_all (count=6) -> [4,3] -> transpose [3,4]
// Rank 0 transposed: [0,4,8,1,5,9,2,6,10,3,7,11]; sends first 6 to self, last 6 to rank 1.
// Rank 1 transposed: [100,104,108,101,105,109,102,106,110,103,107,111]; sends first 6 to rank 0.
// Rank 0 output [4,3]: rows=[0,4,8],[1,5,9],[100,104,108],[101,105,109]; after transpose [3,4]:
// [0,1,100,101, 4,5,104,105, 8,9,108,109]
// Rank 1 output [4,3]: rows=[2,6,10],[3,7,11],[102,106,110],[103,107,111]; after transpose [3,4]:
// [2,3,102,103, 6,7,106,107, 10,11,110,111]
return {CollectiveOperation::kALL_TO_ALL,
{0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F, 10.0F, 11.0F},
{100.0F, 101.0F, 102.0F, 103.0F, 104.0F, 105.0F, 106.0F, 107.0F, 108.0F, 109.0F, 110.0F, 111.0F},
{0.0F, 1.0F, 100.0F, 101.0F, 4.0F, 5.0F, 104.0F, 105.0F, 8.0F, 9.0F, 108.0F, 109.0F}, // rank0
{2.0F, 3.0F, 102.0F, 103.0F, 6.0F, 7.0F, 106.0F, 107.0F, 10.0F, 11.0F, 110.0F, 111.0F}, // rank1
kINPUT_SIZE};
}
case CollectiveOperation::kGATHER:
{
// GATHER: All ranks send to root. Input [3,4] -> transpose [4,3] -> gather [8,3] -> transpose [3,8]
// Root recvbuf = [rank0_data || rank1_data] transposed to [3,8]:
// [0,1,2,3,100,101,102,103, 4,5,6,7,104,105,106,107, 8,9,10,11,108,109,110,111]
// Non-root output buffer is allocated but NCCL does not write to it.
std::vector<float> const rank0Expected = {0.0F, 1.0F, 2.0F, 3.0F, 100.0F, 101.0F, 102.0F, 103.0F, 4.0F,
5.0F, 6.0F, 7.0F, 104.0F, 105.0F, 106.0F, 107.0F, 8.0F, 9.0F, 10.0F, 11.0F, 108.0F, 109.0F, 110.0F,
111.0F};
return {CollectiveOperation::kGATHER,
{0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F, 10.0F, 11.0F},
{100.0F, 101.0F, 102.0F, 103.0F, 104.0F, 105.0F, 106.0F, 107.0F, 108.0F, 109.0F, 110.0F, 111.0F},
rank0Expected,
{}, // rank1's output is undefined (non-root does not receive)
kINPUT_SIZE * worldSize};
}
case CollectiveOperation::kSCATTER:
{
// SCATTER: Root scatters data to all ranks. Input [3,4] -> transpose [4,3].
// Root sendBuf=[0,4,8,1,5,9,2,6,10,3,7,11]; recvCount=6 per rank.
// Rank 0 gets first 6 -> [2,3] -> transpose [3,2]: [0,1,4,5,8,9]
// Rank 1 gets next 6 -> [2,3] -> transpose [3,2]: [2,3,6,7,10,11]
// Rank 1 input is ignored by NCCL (non-root has no sendBuf).
return {CollectiveOperation::kSCATTER,
{0.0F, 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F, 7.0F, 8.0F, 9.0F, 10.0F, 11.0F},
{99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F, 99.0F}, // ignored
{0.0F, 1.0F, 4.0F, 5.0F, 8.0F, 9.0F}, // rank0 expected
{2.0F, 3.0F, 6.0F, 7.0F, 10.0F, 11.0F}, // rank1 expected
kINPUT_SIZE / worldSize};
}
}
throw std::runtime_error("Unknown CollectiveOperation");
}
//! Build and execute a network with a specific collective operation
void testCollectiveOperation(
int32_t rank, int32_t worldSize, CollectiveTestConfig const& config, ncclComm_t comm, cudaStream_t stream)
{
sample::gLogInfo << "Rank " << rank << " - Testing " << toString(config.op) << std::endl;
// Create builder and network
auto builder = std::unique_ptr<IBuilder>(createInferBuilder(sample::gLogger.getTRTLogger()));
ASSERT(builder != nullptr);
auto network = std::unique_ptr<INetworkDefinition>(
builder->createNetworkV2(1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kSTRONGLY_TYPED)));
ASSERT(network != nullptr);
// Create input tensor
constexpr int32_t kINPUT_ROWS = 3;
constexpr int32_t kINPUT_COLS = 4;
auto* input = network->addInput("input", DataType::kFLOAT, Dims2{kINPUT_ROWS, kINPUT_COLS});
ASSERT(input != nullptr);
auto* firstShuffle = network->addShuffle(*input);
ASSERT(firstShuffle != nullptr);
firstShuffle->setFirstTranspose({{1, 0}});
ReduceOperation reduceOp = ReduceOperation::kNONE;
if (config.op == CollectiveOperation::kALL_REDUCE || config.op == CollectiveOperation::kREDUCE
|| config.op == CollectiveOperation::kREDUCE_SCATTER)
{
reduceOp = ReduceOperation::kSUM;
}
int64_t root = -1;
if (config.op == CollectiveOperation::kBROADCAST || config.op == CollectiveOperation::kREDUCE
|| config.op == CollectiveOperation::kGATHER || config.op == CollectiveOperation::kSCATTER)
{
root = 0;
}
auto* collectiveLayer
= network->addDistCollective(*firstShuffle->getOutput(0), config.op, reduceOp, root, nullptr, 0);
ASSERT(collectiveLayer != nullptr);
// Set the number of ranks for the collective operation
if (!collectiveLayer->setNbRanks(worldSize))
{
throw std::runtime_error("Failed to set the number of ranks for the collective layer");
}
auto* secondShuffle = network->addShuffle(*collectiveLayer->getOutput(0));
ASSERT(secondShuffle != nullptr);
secondShuffle->setFirstTranspose({{1, 0}});
// Mark the reshape layer's output as the network output
network->markOutput(*secondShuffle->getOutput(0));
// Build engine
auto builderConfig = std::unique_ptr<IBuilderConfig>(builder->createBuilderConfig());
ASSERT(builderConfig != nullptr);
auto serializedEngine = std::unique_ptr<IHostMemory>(builder->buildSerializedNetwork(*network, *builderConfig));
ASSERT(serializedEngine != nullptr);
// Create runtime and deserialize engine
auto runtime = std::unique_ptr<IRuntime>(createInferRuntime(sample::gLogger.getTRTLogger()));
ASSERT(runtime != nullptr);
// Deserialize the CUDA engine
auto engine = std::unique_ptr<ICudaEngine>(
runtime->deserializeCudaEngine(serializedEngine->data(), serializedEngine->size()));
ASSERT(engine != nullptr);
// Create execution context for the engine
auto context = std::unique_ptr<IExecutionContext>(engine->createExecutionContext());
ASSERT(context != nullptr);
// Prepare input and output buffers
char const* inputName = engine->getIOTensorName(0);
char const* outputName = engine->getIOTensorName(1);
std::vector<float> const& inputChunk = (rank == 0) ? config.rank0Input : config.rank1Input;
std::vector<float> outputChunk(config.outputElementCount, 0.0F);
size_t const inputBytes = inputChunk.size() * sizeof(float);
size_t const outputBytes = outputChunk.size() * sizeof(float);
void* dInput = nullptr;
void* dOutput = nullptr;
CHECK_CUDA(cudaMalloc(&dInput, inputBytes));
CHECK_CUDA(cudaMalloc(&dOutput, outputBytes));
// Copy input data to GPU asynchronously
CHECK_CUDA(cudaMemcpyAsync(dInput, inputChunk.data(), inputBytes, cudaMemcpyHostToDevice, stream));
// Set input/output tensor addresses in the execution context
context->setInputTensorAddress(inputName, dInput);
context->setTensorAddress(outputName, dOutput);
context->setInputShape(inputName, Dims2{kINPUT_ROWS, kINPUT_COLS});
// Set NCCL communicator
if (!context->setCommunicator(comm))
{
cudaFree(dInput);
cudaFree(dOutput);
throw std::runtime_error("Failed to set communicator for " + toString(config.op));
}
// Run inference
if (!context->enqueueV3(stream))
{
cudaFree(dInput);
cudaFree(dOutput);
throw std::runtime_error("Inference failed for " + toString(config.op));
}
CHECK_CUDA(cudaStreamSynchronize(stream));
// Copy output data back to host asynchronously
CHECK_CUDA(cudaMemcpyAsync(outputChunk.data(), dOutput, outputBytes, cudaMemcpyDeviceToHost, stream));
CHECK_CUDA(cudaStreamSynchronize(stream));
// Get the expected output for this rank
std::vector<float> const& expectedOutput = (rank == 0) ? config.rank0ExpectedOutput : config.rank1ExpectedOutput;
// Determine if this rank should verify output
// REDUCE: only rank 0 receives valid result
// All other ops: both ranks can verify (same or different expected values)
bool const shouldVerify = !expectedOutput.empty();
if (shouldVerify)
{
constexpr float kEPS = 1e-5F;
for (size_t i = 0; i < outputChunk.size() && i < expectedOutput.size(); ++i)
{
if (std::abs(outputChunk[i] - expectedOutput[i]) > kEPS)
{
cudaFree(dInput);
cudaFree(dOutput);
throw std::runtime_error("Output mismatch for " + toString(config.op) + " at index " + std::to_string(i)
+ ": expected " + std::to_string(expectedOutput[i]) + ", got " + std::to_string(outputChunk[i]));
}
}
sample::gLogInfo << "Rank " << rank << " - " << toString(config.op) << " PASSED" << std::endl;
}
// Cleanup
cudaFree(dInput);
cudaFree(dOutput);
}
//! Main test function that runs a specific collective operation test
void runCollectiveTest(int32_t rank, int32_t worldSize, CollectiveOperation op)
{
// Check GPU availability
int32_t deviceCount = 0;
CHECK_CUDA(cudaGetDeviceCount(&deviceCount));
if (deviceCount < worldSize)
{
throw std::runtime_error("Not enough GPUs available. Need " + std::to_string(worldSize) + " but found "
+ std::to_string(deviceCount));
}
// Use rank to select GPU
CHECK_CUDA(cudaSetDevice(rank));
// Create CUDA stream
cudaStream_t stream;
CHECK_CUDA(cudaStreamCreate(&stream));
// Set up NCCL - rank 0 generates ID and writes to file, others read from file
ncclUniqueId const id = getNcclIdViaFile(rank);
ncclComm_t comm;
NCCLCHECK(ncclCommInitRank(&comm, worldSize, id, rank));
// Get test configuration for the specified operation
CollectiveTestConfig const config = getTestConfig(op, worldSize);
// Run the collective operation test
testCollectiveOperation(rank, worldSize, config, comm, stream);
sample::gLogInfo << "Rank " << rank << " - " << toString(op) << " test completed successfully!" << std::endl;
NCCLCHECK(ncclCommDestroy(comm));
CHECK_CUDA(cudaStreamDestroy(stream));
}
int main(int argc, char* argv[])
{
constexpr int32_t kREQUIRED_WORLD_SIZE = 2;
for (int32_t i = 1; i < argc; ++i)
{
std::string const arg = argv[i];
if (arg == "--help" || arg == "-h")
{
printUsage(argv[0]);
return 0;
}
}
// Get rank and world size from TRT_MY_RANK and TRT_WORLD_SIZE environment variables.
int32_t const rank = getRankFromEnv();
int32_t const worldSize = getWorldSizeFromEnv();
// Parse command line arguments
CollectiveOperation selectedOp{};
bool hasSelectedOp = false;
for (int32_t i = 1; i < argc; ++i)
{
std::string arg = argv[i];
if (arg == "--op" && i + 1 < argc)
{
++i;
auto parsedOp = parseCollectiveOp(argv[i]);
if (!parsedOp)
{
if (rank == 0)
{
sample::gLogError << "Invalid operation: " << argv[i] << std::endl;
printUsage(argv[0]);
}
return 1;
}
selectedOp = *parsedOp;
hasSelectedOp = true;
}
}
// --op is required
if (!hasSelectedOp)
{
if (rank == 0)
{
sample::gLogError << "Error: --op argument is required." << std::endl;
printUsage(argv[0]);
}
return 1;
}
// We need exactly 2 processes for this test
if (worldSize != kREQUIRED_WORLD_SIZE)
{
if (rank == 0)
{
sample::gLogError << "This sample requires exactly 2 processes, but " << worldSize << " were provided."
<< std::endl;
sample::gLogError << "Please set TRT_WORLD_SIZE=2 and launch 2 processes." << std::endl;
sample::gLogError << "Run with --help for example commands." << std::endl;
}
return 1;
}
try
{
runCollectiveTest(rank, worldSize, selectedOp);
}
catch (std::exception const& e)
{
sample::gLogError << "Rank " << rank << " - Exception: " << e.what() << std::endl;
return 1;
}
return 0;
}