/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 // 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; #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 pCreateInferRuntimeInternal{}; std::function pCreateInferRefitterInternal{}; std::function pCreateInferBuilderInternal{}; std::function pCreateNvOnnxParserInternal{}; std::function 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 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("createInferRuntime_INTERNAL"); try { pCreateInferRefitterInternal = l->symbolAddress("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("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("createNvOnnxParser_INTERNAL"); pCreateNvOnnxRefitterInternal = l->symbolAddress("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(a)) == std::tolower(static_cast(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 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 " << std::endl; std::cout << std::endl; std::cout << "Options:" << std::endl; std::cout << " --op 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(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((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(pCreateInferRuntimeInternal(&gLogger.getTRTLogger(), NV_TENSORRT_VERSION)); } IBuilder* createBuilder() { if (!initNvinfer()) { return {}; } ASSERT(pCreateInferBuilderInternal != nullptr); return static_cast(pCreateInferBuilderInternal(&gLogger.getTRTLogger(), NV_TENSORRT_VERSION)); } IRefitter* createRefitter(ICudaEngine& engine) { if (!initNvinfer()) { return {}; } ASSERT(pCreateInferRefitterInternal != nullptr); return static_cast(pCreateInferRefitterInternal(&engine, &gLogger.getTRTLogger(), NV_TENSORRT_VERSION)); } nvonnxparser::IParser* createONNXParser(INetworkDefinition& network) { if (!initNvonnxparser()) { return {}; } ASSERT(pCreateNvOnnxParserInternal != nullptr); return static_cast( pCreateNvOnnxParserInternal(&network, &gLogger.getTRTLogger(), NV_ONNX_PARSER_VERSION)); } nvonnxparser::IParserRefitter* createONNXRefitter(IRefitter& refitter) { if (!initNvonnxparser()) { return {}; } ASSERT(pCreateNvOnnxRefitterInternal != nullptr); return static_cast( pCreateNvOnnxRefitterInternal(&refitter, &gLogger.getTRTLogger(), NV_ONNX_PARSER_VERSION)); } //! Helper struct to hold test configuration for each collective operation struct CollectiveTestConfig { CollectiveOperation op; std::vector rank0Input; std::vector rank1Input; std::vector rank0ExpectedOutput; std::vector 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 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 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 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 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(createInferBuilder(sample::gLogger.getTRTLogger())); ASSERT(builder != nullptr); auto network = std::unique_ptr( builder->createNetworkV2(1U << static_cast(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(builder->createBuilderConfig()); ASSERT(builderConfig != nullptr); auto serializedEngine = std::unique_ptr(builder->buildSerializedNetwork(*network, *builderConfig)); ASSERT(serializedEngine != nullptr); // Create runtime and deserialize engine auto runtime = std::unique_ptr(createInferRuntime(sample::gLogger.getTRTLogger())); ASSERT(runtime != nullptr); // Deserialize the CUDA engine auto engine = std::unique_ptr( runtime->deserializeCudaEngine(serializedEngine->data(), serializedEngine->size())); ASSERT(engine != nullptr); // Create execution context for the engine auto context = std::unique_ptr(engine->createExecutionContext()); ASSERT(context != nullptr); // Prepare input and output buffers char const* inputName = engine->getIOTensorName(0); char const* outputName = engine->getIOTensorName(1); std::vector const& inputChunk = (rank == 0) ? config.rank0Input : config.rank1Input; std::vector 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 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; }