/* * 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 "NvInferSafeRuntime.h" // TRTS-10206: NvInferSafeRuntime.h may be refactored #include "cuda_runtime.h" #include "delayStreamKernel.h" #include "safeCommon.h" #include "safeCudaAllocator.h" #include "safeErrorRecorder.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace nvinfer1; using namespace samplesSafeCommon; using SafetyPluginArguments = std::vector; __attribute__((weak)) std::shared_ptr gSafeRecorder = std::make_shared(nvinfer2::safe::Severity::kINFO); //! //! \brief The TimingMetric struct stores the timing metric of a performance metric //! //! \param[in] gpuTime: GPU time in milliseconds. //! \param[in] hostTime: Host time in milliseconds. //! \param[in] enqueueTime: Enqueue time in milliseconds. //! using TimingMetric = std::array; using TimingMetrics = std::vector; //! //! \brief The SafeExecArgs struct stores the arguments required by the sample //! class SafeExecArgs { public: std::string engineFile{"sample.engine"}; int32_t iterations{10}; int32_t avgRuns{10}; int32_t warmUp{1}; int32_t device{0}; int32_t streams{1}; float idle{0.F}; float duration{3.F}; float sleep{2.F}; float percentile{99.F}; bool spin{false}; bool verbose{false}; bool debug{false}; bool help{false}; bool useCudaGraph{true}; bool useScratchMemory{false}; int32_t threads{1}; int64_t ioProfile{0}; SafetyPluginArguments pluginLibraries; std::unordered_map loadInputs; //!< Map of tensor names to file paths for loading custom input data }; //! //! \brief The PerformanceResult struct stores the performance result of a performance metric //! class SafePerformanceResult { public: float min{0.F}; float max{0.F}; float mean{0.F}; float median{0.F}; float percentile{0.F}; float coeffVar{0.F}; }; namespace { [[nodiscard]] constexpr bool startsWith(std::string_view a, std::string_view b) { return a.size() >= b.size() && a.substr(0, b.size()) == b; } //! Default alignment for memory allocations constexpr uint64_t kDEFAULT_ALIGNMENT{256U}; //! //! \brief RAII wrapper for SafeMemAllocator to ensure automatic cleanup. //! class ScopedSafeMemory { public: ScopedSafeMemory(uint64_t size, uint64_t alignment, nvinfer2::safe::MemoryPlacement placement, nvinfer2::safe::MemoryUsage usage, nvinfer2::safe::ISafeRecorder& recorder) : mPtr(nullptr) , mPlacement(placement) , mRecorder(recorder) { auto& allocator = nvinfer2::safe::getSafeMemAllocator(); mPtr = allocator.allocate(size, alignment, placement, usage, recorder); } ~ScopedSafeMemory() { if (mPtr) { auto& allocator = nvinfer2::safe::getSafeMemAllocator(); allocator.deallocate(mPtr, mPlacement, mRecorder); } } ScopedSafeMemory(ScopedSafeMemory const&) = delete; ScopedSafeMemory& operator=(ScopedSafeMemory const&) = delete; ScopedSafeMemory(ScopedSafeMemory&& other) noexcept : mPtr(other.mPtr) , mPlacement(other.mPlacement) , mRecorder(other.mRecorder) { other.mPtr = nullptr; } ScopedSafeMemory& operator=(ScopedSafeMemory&& other) noexcept { if (this != &other) { // Clean up existing resource auto& allocator = nvinfer2::safe::getSafeMemAllocator(); allocator.deallocate(mPtr, mPlacement, mRecorder); // Transfer ownership mPtr = other.mPtr; mPlacement = other.mPlacement; other.mPtr = nullptr; } return *this; } void* get() const noexcept { return mPtr; } explicit operator bool() const noexcept { return mPtr != nullptr; } bool operator==(ScopedSafeMemory const& other) const noexcept { return mPtr == other.mPtr; } bool operator!=(ScopedSafeMemory const& other) const noexcept { return mPtr != other.mPtr; } private: void* mPtr; nvinfer2::safe::MemoryPlacement mPlacement; nvinfer2::safe::ISafeRecorder& mRecorder; }; //! Similar to C++20 template function std::ssize. template constexpr auto signedSize(C const& c) -> std::common_type_t> { /* polyspace +2 RTE:OVFL [Justified:Low] */ return static_cast>>(c.size()); } std::optional loggedParseString(std::string const& arg, std::string const& name) { auto result = parseString(arg, name); if (result) { safeLogInfo(*gSafeRecorder, name + " : " + *result); } return result; } bool loggedParseBool(std::string const& arg, std::string const& name, std::optional singleChar = {}) { bool result = parseBool(arg, name, singleChar); if (result) { safeLogInfo(*gSafeRecorder, name + " : True"); } return result; } //! //! \brief Get the percentile of a performance metric //! //! \param[in] percentage: Percentile to get //! \param[in] times: Measurement times in milliseconds. //! \param[in] metricIndex: Index of performance measurement metrics //! //! \return The percentile of a performance metric float percentile(float percentage, TimingMetrics const& times, int32_t metricIndex) { int32_t const all = static_cast(times.size()); int32_t const exclude = static_cast((1 - percentage / 100.F) * all); if (times.empty()) { return std::numeric_limits::infinity(); } if (percentage < 0.F || percentage > 100.F) { throw std::runtime_error("percentile is not in [0, 100]!"); } return times[std::max(all - 1 - exclude, 0)][metricIndex]; } //! //! \brief Find coefficient of variance (which is std / mean) in a sorted sequence of timings //! //! \param[in] times: Measurement times in milliseconds. //! \param[in] metricIndex: Index of performance measurement metrics //! \param[in] mean: Mean of the performance measurement metrics //! //! \return The coefficient of variance float findCoeffOfVariance(TimingMetrics const& times, int32_t metricIndex, float mean) { if (times.empty()) { return 0.F; } if (mean == 0.F) { return std::numeric_limits::infinity(); } auto const metricAccumulator = [metricIndex, mean](float acc, TimingMetric const& a) { float const diff = a[metricIndex] - mean; return acc + diff * diff; }; float const variance = std::accumulate(times.begin(), times.end(), 0.F, metricAccumulator) / times.size(); return std::sqrt(variance) / mean * 100.F; } //! //! \brief Get the performance result of a performance metric //! //! \param[in] times: Measurement times in milliseconds. //! \param[in] metricIndex: Index of performance measurement metrics //! \param[in] percent: Percentile to get //! //! \return The performance result of a performance metric SafePerformanceResult getSafePerformanceResult(TimingMetrics const& times, int32_t metricIndex, float percent) { auto const ascendingSorter = [metricIndex](TimingMetric& a, TimingMetric& b) { return a[metricIndex] < b[metricIndex]; }; // make a copy w/o const qualifier TimingMetrics newTimes = times; std::sort(newTimes.begin(), newTimes.end(), ascendingSorter); SafePerformanceResult result; result.min = newTimes[0][metricIndex]; result.max = newTimes[newTimes.size() - 1][metricIndex]; result.mean = std::accumulate(newTimes.begin(), newTimes.end(), 0.F, [metricIndex](float acc, TimingMetric& a) { return acc + a[metricIndex]; }) / newTimes.size(); size_t const medianIndex = newTimes.size() / 2ULL; result.median = newTimes.size() % 2ULL ? newTimes[medianIndex][metricIndex] : (newTimes[medianIndex][metricIndex] + newTimes[medianIndex + 1ULL][metricIndex]) / 2.0f; result.percentile = percentile(percent, newTimes, metricIndex); result.coeffVar = findCoeffOfVariance(newTimes, metricIndex, result.mean); return result; } nvinfer2::safe::TypedArray createTypedArray( void* const ptr, DataType type, uint64_t bufferSize, nvinfer2::safe::ISafeRecorder& recorder) { switch (type) { case DataType::kFLOAT: return nvinfer2::safe::TypedArray(static_cast(ptr), bufferSize); case DataType::kHALF: return nvinfer2::safe::TypedArray(static_cast(ptr), bufferSize); case DataType::kINT64: return nvinfer2::safe::TypedArray(static_cast(ptr), bufferSize); case DataType::kINT32: return nvinfer2::safe::TypedArray(static_cast(ptr), bufferSize); case DataType::kINT8: return nvinfer2::safe::TypedArray(static_cast(ptr), bufferSize); case DataType::kUINT8: return nvinfer2::safe::TypedArray(static_cast(ptr), bufferSize); case DataType::kBOOL: return nvinfer2::safe::TypedArray(static_cast(ptr), bufferSize); default: { safeLogError(recorder, "Invalid tensor DataType encountered."); return nvinfer2::safe::TypedArray{}; } } } //! //! \brief Allocate memory and memset it to zero using safe CUDA-compatible APIs. //! //! \param[in] sizeInBytes The size of memory to allocate in bytes //! \param[in] recorder The safe recorder for error logging and API calls //! //! \return ScopedSafeMemory object containing the allocated zeroed memory //! ScopedSafeMemory allocateAndMemset(uint64_t sizeInBytes, nvinfer2::safe::ISafeRecorder& recorder) { ScopedSafeMemory deviceBuf(sizeInBytes, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kGPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); if (!deviceBuf) { return deviceBuf; } // Use async memset and synchronize (required for QNX safety builds where cudaMemset is not available) cudaStream_t stream; CUDA_CALL(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking), recorder); CUDA_CALL(cudaMemsetAsync(deviceBuf.get(), 0, sizeInBytes, stream), recorder); CUDA_CALL(cudaStreamSynchronize(stream), recorder); CUDA_CALL(cudaStreamDestroy(stream), recorder); return deviceBuf; } //! //! \brief Load data from a binary file into a pre-allocated buffer. //! //! This function reads data from a binary file and validates the file size matches //! the expected buffer size. It provides detailed error reporting for file operations. //! //! \param[in] fileName The path to the binary file to load //! \param[out] buffer Pointer to the buffer to load data into //! \param[in] sizeInBytes The expected size of the file and buffer in bytes //! \param[in] recorder The safe recorder for error logging //! //! \return True if file was loaded successfully, false otherwise //! bool loadDataFromFile( std::string const& fileName, void* buffer, uint64_t sizeInBytes, nvinfer2::safe::ISafeRecorder& recorder) { std::ifstream file(fileName, std::ios::in | std::ios::binary); if (!file.is_open()) { safeLogError(recorder, "Cannot open input file: " + fileName); return false; } file.seekg(0, std::ios::end); int64_t fileSize = static_cast(file.tellg()); if (fileSize != static_cast(sizeInBytes)) { file.close(); std::ostringstream msg; msg << "File size mismatch for " << fileName << ". Expected: " << sizeInBytes << " bytes, got: " << fileSize << " bytes"; safeLogError(recorder, msg.str()); return false; } file.seekg(0, std::ios::beg); file.read(reinterpret_cast(buffer), sizeInBytes); size_t const nbBytesRead = file.gcount(); file.close(); if (nbBytesRead != sizeInBytes) { std::ostringstream msg; msg << "Failed to read complete file " << fileName << ". Expected: " << sizeInBytes << " bytes, read: " << nbBytesRead << " bytes"; safeLogError(recorder, msg.str()); return false; } return true; } //! //! \brief Allocate memory and load data from file using safe CUDA-compatible APIs. //! //! This function allocates GPU memory and loads data from a binary file into it. //! It performs file size validation and uses RAII for automatic memory cleanup. //! //! \param[in] sizeInBytes The size of memory to allocate in bytes //! \param[in] fileName The path to the binary file to load //! \param[in] recorder The safe recorder for error logging and API calls //! //! \return ScopedSafeMemory object containing the loaded data, or an invalid object on failure //! ScopedSafeMemory allocateAndLoadFromFile( uint64_t sizeInBytes, std::string const& fileName, nvinfer2::safe::ISafeRecorder& recorder) { // Allocate pinned host memory for temporary storage with RAII ScopedSafeMemory hostBuf(sizeInBytes, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kCPU_PINNED, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); if (!hostBuf) { safeLogError(recorder, "Failed to allocate host memory for input file: " + fileName); return ScopedSafeMemory(0, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kGPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); } // Load data from file into host buffer if (!loadDataFromFile(fileName, hostBuf.get(), sizeInBytes, recorder)) { return ScopedSafeMemory(0, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kGPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); } // Allocate device memory with RAII and copy data ScopedSafeMemory deviceBuf(sizeInBytes, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kGPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); if (!deviceBuf) { safeLogError(recorder, "Failed to allocate device memory for input file: " + fileName); return ScopedSafeMemory(0, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kGPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); } // Use async copy and synchronize (required for QNX safety builds where cudaMemcpy may not be available) cudaStream_t stream; CUDA_CALL(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking), recorder); CUDA_CALL(cudaMemcpyAsync(deviceBuf.get(), hostBuf.get(), sizeInBytes, cudaMemcpyHostToDevice, stream), recorder); CUDA_CALL(cudaStreamSynchronize(stream), recorder); CUDA_CALL(cudaStreamDestroy(stream), recorder); return deviceBuf; } //! //! \brief Parse loadInputs string in the format "name1:file1,name2:file2,..." //! //! This function parses a comma-separated string of input tensor name to file path mappings. //! It supports quoted tensor names (e.g., 'input_name':file.bin) to handle names with special characters. //! The format follows the same convention as standard trtexec --loadInputs parameter. //! //! \param[in] loadInputsStr The input string to parse in format "name1:file1,name2:file2,..." //! //! \return A map containing tensor names as keys and file paths as values. Returns empty map if input is invalid. //! std::unordered_map parseLoadInputs(std::string const& loadInputsStr) { std::unordered_map result; if (loadInputsStr.empty()) { return result; } // Split by comma std::stringstream ss(loadInputsStr); std::string pair; while (std::getline(ss, pair, ',')) { // Handle quoted names (e.g., 'input_name':file.bin) std::string tensorName; std::string fileName; size_t colonPos = pair.find_last_of(':'); if (colonPos == std::string::npos) { safeLogDebug(*gSafeRecorder, "Invalid input pair skipped: \"" + pair + "\" (reason: no ':' separator found - expected format 'tensorName:fileName')"); continue; // Skip invalid pairs } tensorName = pair.substr(0, colonPos); fileName = pair.substr(colonPos + 1); // Remove quotes if present if (tensorName.size() >= 2 && tensorName.front() == '\'' && tensorName.back() == '\'') { tensorName = tensorName.substr(1, tensorName.size() - 2); } result[tensorName] = fileName; } return result; } bool parseSafetyPluginLibrary( std::string const& arg, std::string const& name, SafetyPluginLibraryArgument& pluginLibArgs) { std::string const pattern = "--" + name + "="; bool const matched = startsWith(arg, pattern); bool status{false}; if (matched) { std::string const optionStr = arg.substr(pattern.size()); status = parseSafetyPluginArgument(optionStr, pluginLibArgs); if (!status) { safeLogError(*gSafeRecorder, "Unable to parse safety plugin library argument: " + arg); } } return matched && status; } // Use template to allow volume for either nvinfer1::Dims or nvinfer2::safe::PhysicalDims template int64_t volume(TDims const& dims, TDims const& strides, uint64_t bytesPerComponent) { if (dims.nbDims == 0 || strides.nbDims == 0) { return 0; } // product of all tensor dimensions int64_t volume = 1; for (int64_t i = 0; i < dims.nbDims; i++) { if (dims.d[i] < 1) { return 0; } SAFE_ASSERT(volume <= INT64_MAX / dims.d[i]); volume *= dims.d[i]; } // real tensor volume is the max between the product of all dimensions and the dims.n * strides.n SAFE_ASSERT(dims.d[0] <= INT64_MAX / strides.d[0]); volume = std::max(volume, dims.d[0] * strides.d[0]); return volume * bytesPerComponent; } } // anonymous namespace //! //! \brief This function parses arguments specific to the sample //! // NOLINTNEXTLINE(readability-function-cognitive-complexity) bool parseSafeExecArgs(SafeExecArgs& args, int32_t argc, char* argv[]) { safeLogInfo(*gSafeRecorder, "Parsing input arguments..."); for (int32_t i = 1; i < argc; ++i) { std::string const arg = argv[i]; if (auto value = loggedParseString(arg, "loadEngine")) { args.engineFile = std::move(*value); } else if (SafetyPluginLibraryArgument pluginArg; parseSafetyPluginLibrary(arg, "safetyPlugins", pluginArg)) { args.pluginLibraries.emplace_back(std::move(pluginArg)); } else if (auto const value = loggedParseString(arg, "iterations")) { args.iterations = stoi(*value); } else if (auto const value = loggedParseString(arg, "avgRuns")) { args.avgRuns = stoi(*value); } else if (auto const value = loggedParseString(arg, "warmUp")) { args.warmUp = stoi(*value); } else if (auto const value = loggedParseString(arg, "device")) { args.device = stoi(*value); } else if (auto const value = loggedParseString(arg, "percentile")) { args.percentile = stof(*value); } else if (auto const value = loggedParseString(arg, "idleTime")) { args.idle = stof(*value); } else if (auto const value = loggedParseString(arg, "duration")) { args.duration = stof(*value); } else if (auto const value = loggedParseString(arg, "sleepTime")) { args.sleep = stof(*value); } else if (loggedParseBool(arg, "spin")) { args.spin = true; } else if (loggedParseBool(arg, "verbose")) { args.verbose = true; } else if (loggedParseBool(arg, "debug")) { args.debug = true; } else if (loggedParseBool(arg, "help", 'h')) { args.help = true; } else if (loggedParseBool(arg, "useCudaGraph")) { // Deprecated: CUDA graph is now enabled by default. safeLogWarning(*gSafeRecorder, "--useCudaGraph is deprecated (now enabled by default). Use --noCudaGraph to disable."); } else if (loggedParseBool(arg, "noCudaGraph")) { args.useCudaGraph = false; } else if (auto const value = loggedParseString(arg, "threads")) { args.threads = stoi(*value); } else if (loggedParseBool(arg, "useScratch")) { args.useScratchMemory = true; } else if (loggedParseBool(arg, "separateProfileRun")) { // Deprecated: separate profile run is now always enabled. safeLogWarning(*gSafeRecorder, "--separateProfileRun is deprecated (now always enabled). This flag will be removed in a future " "release."); } else if (auto const value = loggedParseString(arg, "ioProfileId")) { // Select I/O profile index for the TRTGraph args.ioProfile = std::stoll(*value); if (args.ioProfile < 0) { safeLogError(*gSafeRecorder, "Invalid ioProfileId (must be >= 0): " + *value); return false; } } else if (auto const value = loggedParseString(arg, "loadInputs")) { args.loadInputs = parseLoadInputs(*value); if (!value->empty() && args.loadInputs.empty()) { safeLogError(*gSafeRecorder, "Invalid loadInputs format: " + *value); return false; } } else { safeLogError(*gSafeRecorder, "Invalid Argument: " + arg); return false; } } bool const hasRequired = !args.engineFile.empty(); if (!hasRequired && !args.help) { safeLogError(*gSafeRecorder, "Engine file is required."); return false; } return true; } //! //! \brief Prints the help information for running this sample. //! void printHelpInfo() { SafeExecArgs const defArgs{}; std::cout << R"(Usage: trtexec_safe --loadEngine= [options] Required params: --loadEngine=FILE Load the serialized engine from FILE. General optional params: --help or -h Display help information --verbose Use verbose logging --debug Use debug logging --useScratch Use separately allocated scratch memory --safetyPlugins=spec Load safety plugin libraries (can be specified multiple times) Plugin spec ::= pluginLib[pluginNamespace::pluginName],[...] Example: --safetyPlugins=myPlugin.so[MyNamespace::MyPlugin] --loadInputs=spec Load input values from files (default = generate zero inputs). Input names can be wrapped with single quotes (ex: 'Input:0') Input values spec ::= Ival[\",\"spec] Ival ::= name\":\"file Example: --loadInputs=\"input1\":data1.bin,\"input2\":data2.bin Perf measurement params: --device=N Set cuda device to N (default = )" << defArgs.device << R"() --threads=N Run in N threads (default = )" << defArgs.threads << R"() --spin Actively wait for work completion. This may decrease multi-process synchronization time at the cost of additional CPU usage. (default = false) --iterations=N Run N iterations (default = )" << defArgs.iterations << R"() --avgRuns=N Set avgRuns to N - perf is measured as an average of avgRuns (default = )" << defArgs.avgRuns << R"() --warmUp=N Run N iterations before actual perf measurement (default = )" << defArgs.warmUp << R"() --idleTime=F Sleep F milliseconds between two continuous iterations (default = )" << defArgs.idle << R"() --percentile=P For each iteration, report the percentile time at P percentage (0<=P<=100, with 0 representing min, and 100 representing max; default = )" << defArgs.percentile << R"(%) --noCudaGraph Disable CUDA graph capture and launch (default = CUDA graph enabled) --useCudaGraph [Deprecated] CUDA graph is now enabled by default. This flag is a no-op. --duration=F Run performance measurements for at least F seconds of wallclock time (default = )" << defArgs.duration << R"(s) --sleepTime=F Delay inference start with a gap of F msec between launch and compute (default = )" << defArgs.sleep << R"() --separateProfileRun [Deprecated] Separate profile run is now always enabled. This flag is a no-op. I/O profile params: --ioProfileId=N Select the I/O profile index to use (default = )" << defArgs.ioProfile << R"() )"; } void registerSafetyPlugins(nvinfer2::safe::ISafeRecorder& recorder, SafetyPluginArguments const& pluginArgs) { std::string const pluginGetterSymbolName{"getSafetyPluginCreator"}; auto const safePluginRegistry = nvinfer2::safe::getSafePluginRegistry(recorder); if (!safePluginRegistry) { safeLogError(recorder, "Safe Plugin Registry is not found."); return; } for (auto const& pluginArg : pluginArgs) { void* libraryHandle = safeLoadLibrary(pluginArg.libraryName); if (libraryHandle == nullptr) { safeLogError(recorder, "Not able to load plugin library: " + pluginArg.libraryName); continue; } typedef IPluginCreatorInterface* (*getPluginCreatorFn)(char const*, char const*); auto pluginCreatorGetter = reinterpret_cast(dlsym(libraryHandle, pluginGetterSymbolName.c_str())); if (pluginCreatorGetter == nullptr) { safeLogError( recorder, "Cannot find plugin creator getter symbol from plugin library: " + pluginArg.libraryName); safeLogError(recorder, "Please ensure interface function is correctly implemented and exported."); continue; } for (auto const& pluginAttr : pluginArg.pluginAttrs) { auto pluginCreator = static_cast( pluginCreatorGetter(pluginAttr.pluginNamespace.c_str(), pluginAttr.pluginName.c_str())); if (pluginCreator == nullptr) { safeLogWarning(recorder, "Plugin interface getSafetyPluginCreator return nullptr for " + pluginAttr.pluginNamespace + "::" + pluginAttr.pluginName + " in the safety plugin library: " + pluginArg.libraryName); safeLogWarning(recorder, "Please ensure interface function is implemented correctly and plugin name/namespace is matched."); continue; } safeLogInfo(recorder, "Registering " + pluginAttr.pluginNamespace + "::" + pluginAttr.pluginName); ErrorCode errorCode = safePluginRegistry->registerCreator(*pluginCreator, pluginAttr.pluginNamespace.c_str(), recorder); if (errorCode != ErrorCode::kSUCCESS) { safeLogWarning(recorder, "Failed to register safety plugin " + pluginAttr.pluginNamespace + "::" + pluginAttr.pluginName); if (errorCode == ErrorCode::kINVALID_ARGUMENT) { safeLogWarning(recorder, "Is getPluginName/getPluginNamespace/getPluginVersion interface implemented and return " "non-nullptr?"); } } } } } //! //! \brief Load a prebuilt TensorRT safe engine. //! std::vector loadEngine(std::string const& engineFile) { std::string const& filename = engineFile; std::vector modelBuffer; std::ifstream file(filename, std::ios::binary); if (!file.good()) { safeLogError(*gSafeRecorder, "Could not open input engine file or file is empty. File name: " + filename); return modelBuffer; } file.seekg(0, std::ifstream::end); auto size = file.tellg(); file.seekg(0, std::ifstream::beg); modelBuffer.resize(size); file.read(modelBuffer.data(), size); file.close(); return modelBuffer; } //! //! \brief Common helper function to set up tensor buffer with optional file loading. //! //! This function handles the common logic for setting up tensor buffers, including //! memory allocation, optional file loading, and tensor address assignment. //! //! \param[in] graph Pointer to the TRT graph //! \param[in] recorder The safe recorder for error logging and API calls //! \param[in] desc The tensor descriptor containing size and memory placement info //! \param[in] tensorName The name of the tensor for logging and loadInputs lookup //! \param[in] loadInputs Optional map of tensor names to file paths for loading custom input data //! //! \return ScopedSafeMemory object containing the allocated tensor buffer, or an invalid object on failure //! ScopedSafeMemory setupTensorBuffer(nvinfer2::safe::ITRTGraph* graph, nvinfer2::safe::ISafeRecorder& recorder, nvinfer2::safe::TensorDescriptor const& desc, std::string const& tensorName, std::unordered_map const& loadInputs) { std::stringstream ss; bool const onGpu = desc.memPlacement == nvinfer2::safe::MemoryPlacement::kGPU || desc.memPlacement == nvinfer2::safe::MemoryPlacement::kNONE; // Calculate expected size using volume calculation from upstream. // Tensor volume could be zero if using MSS engine build. uint64_t const expectedSize = std::max(static_cast(volume(desc.shape, desc.stride, desc.bytesPerComponent)), desc.sizeInBytes); // Check if we have input data to load for this tensor auto const inputIt = loadInputs.find(tensorName); bool const hasInputFile = (inputIt != loadInputs.end() && !tensorName.empty()); if (onGpu) { ScopedSafeMemory deviceBuf = hasInputFile ? allocateAndLoadFromFile(expectedSize, inputIt->second, recorder) : allocateAndMemset(expectedSize, recorder); if (hasInputFile) { ss << "Loaded input data from " << inputIt->second << " for tensor " << tensorName; safeLogInfo(recorder, ss.str()); ss.str(""); } ss << "Set address of " << tensorName << " on device at " << std::hex << (uint64_t) deviceBuf.get() << std::dec; safeLogInfo(recorder, ss.str()); return deviceBuf; } else if (desc.memPlacement == nvinfer2::safe::MemoryPlacement::kCPU) { ScopedSafeMemory hostBuf(expectedSize, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kCPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); if (!hostBuf) { safeLogError(recorder, "Failed to allocate host memory for tensor: " + tensorName); return ScopedSafeMemory(0, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kCPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); } if (hasInputFile) { // Load data from file for CPU tensors if (!loadDataFromFile(inputIt->second, hostBuf.get(), expectedSize, recorder)) { safeLogError(recorder, "Failed to load input file for tensor: " + tensorName); return ScopedSafeMemory(0, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kCPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); } ss << "Loaded input data from " << inputIt->second << " for tensor " << tensorName; safeLogInfo(recorder, ss.str()); ss.str(""); } else { memset(hostBuf.get(), 0, expectedSize); } ss << "Set address of " << tensorName << " on host at " << std::hex << (uint64_t) hostBuf.get() << std::dec; safeLogInfo(recorder, ss.str()); return hostBuf; } else { safeLogError(recorder, "Invalid memory placement for tensor: " + tensorName); return ScopedSafeMemory(0, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kGPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); } } //! //! \brief Set I/O tensor buffer with optional input file loading. //! //! This function allocates memory for a tensor and optionally loads data from a file. //! If the tensor name is found in the loadInputs map, it loads data from the specified file. //! Otherwise, it initializes the tensor with zeros. Supports both GPU and CPU memory placement. //! //! \param[in] graph Pointer to the TRT graph //! \param[in] recorder The safe recorder for error logging and API calls //! \param[in] tensorName The name of the tensor to set up //! \param[in] loadInputs Optional map of tensor names to file paths for loading custom input data //! //! \return ScopedSafeMemory object containing the allocated tensor buffer //! ScopedSafeMemory setTensorBuffer(nvinfer2::safe::ITRTGraph* graph, nvinfer2::safe::ISafeRecorder& recorder, std::string const& tensorName, std::unordered_map const& loadInputs = {}) { nvinfer2::safe::TensorDescriptor desc; SAFE_API_CALL(graph->getIOTensorDescriptor(desc, tensorName.c_str()), recorder); // Use common helper to set up the tensor buffer ScopedSafeMemory tensorBuffer = setupTensorBuffer(graph, recorder, desc, tensorName, loadInputs); if (!tensorBuffer) { return ScopedSafeMemory(0, kDEFAULT_ALIGNMENT, nvinfer2::safe::MemoryPlacement::kGPU, nvinfer2::safe::MemoryUsage::kIOTENSOR, recorder); } // Tensor volume could be zero if using MSS engine build. uint64_t expectedSize = std::max(static_cast(volume(desc.shape, desc.stride, desc.bytesPerComponent)), desc.sizeInBytes); // Set the tensor address in the graph nvinfer2::safe::TypedArray const tensor = createTypedArray(tensorBuffer.get(), desc.dataType, expectedSize, recorder); SAFE_API_CALL(graph->setIOTensorAddress(tensorName.c_str(), tensor), recorder); return tensorBuffer; } //! \brief Function to CUDA Graph capture bool graphCapture(cudaStream_t stream, TrtCudaGraphSafe& cudaGraph, nvinfer2::safe::ITRTGraph* graph, nvinfer2::safe::ISafeRecorder& recorder) { // Avoid capturing initialization calls by executing the enqueue function at least // once before starting CUDA graph capture. ErrorCode executeRes = graph->executeAsync(stream); ErrorCode syncRes = graph->sync(); if (executeRes != nvinfer1::ErrorCode::kSUCCESS || syncRes != nvinfer1::ErrorCode::kSUCCESS) { safeLogError(recorder, "The enqueue function before starting CUDA graph capture failed."); return false; } static_cast(cudaStreamSynchronize(stream)); cudaGraph.beginCapture(stream); // The built TRT engine may contain operations that are not permitted under CUDA graph capture mode. // When the stream is capturing, the enqueue call may return false if the current CUDA graph capture fails. executeRes = graph->executeAsync(stream); if (executeRes == nvinfer1::ErrorCode::kSUCCESS) { cudaGraph.endCapture(stream); } else { cudaGraph.endCaptureOnError(stream); // Ensure any CUDA error has been cleaned up. CUDA_CHECK(cudaGetLastError()); safeLogError(recorder, "The built TensorRT engine contains operations that are not permitted under CUDA graph capture mode."); return false; } return true; } //! //! \brief Thread task to run graph execution with optional input file loading. //! //! This function sets up tensor buffers (optionally loading from files specified in args.loadInputs), //! executes the graph for the specified number of iterations, and measures performance. //! It handles both profiling runs and regular inference runs. //! //! \param[in] args The execution arguments containing loadInputs map and other configuration //! \param[in] graph Pointer to the TRT graph to execute //! \param[in] recorder Pointer to the safe recorder for error logging and API calls //! \param[in] isProfileRun Whether this is a profiling run or regular inference //! //! \return True if execution completed successfully, false otherwise //! bool task(SafeExecArgs const& args, nvinfer2::safe::ITRTGraph* graph, nvinfer2::safe::ISafeRecorder* recorder, bool isProfileRun) { int64_t nbIOs{}; SAFE_API_CALL(graph->getNbIOTensors(nbIOs), *recorder); std::vector buffers; buffers.reserve(nbIOs); // Set input tensor values for (int64_t i = 0; i < nbIOs; ++i) { char const* tensor; SAFE_API_CALL(graph->getIOTensorName(tensor, i), *recorder); buffers.emplace_back(setTensorBuffer(graph, *recorder, tensor, args.loadInputs)); } cudaEvent_t inputConsumedEvent; cudaEventCreate(&inputConsumedEvent); SAFE_API_CALL(graph->setInputConsumedEvent(inputConsumedEvent), *recorder); cudaEvent_t retrievedEvent; SAFE_API_CALL(graph->getInputConsumedEvent(retrievedEvent), *recorder); SAFE_ASSERT(retrievedEvent != nullptr); cudaEventSynchronize(retrievedEvent); // Initialize main stream cudaStream_t stream; CUDA_CALL(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking), *recorder); // Setup as many auxiliary streams as the graph requires - destroyed at scope end. auto auxStreamsDeleter = samplesSafeCommon::setUpAuxStreamsOn(*graph, *recorder); uint32_t const cudaEventFlags = args.spin ? cudaEventDefault : cudaEventBlockingSync; cudaEvent_t gpuStart; CUDA_CALL(cudaEventCreateWithFlags(&gpuStart, cudaEventFlags), *recorder); CUDA_CALL(cudaEventRecord(gpuStart, stream), *recorder); float absStartTime{0.0f}; float absEndTime{0.0f}; if (!isProfileRun) { safeLogInfo(*recorder, "Starting inference..."); } // Warm up for (int32_t i = 0; i < args.warmUp; i++) { SAFE_API_CALL(graph->executeAsync(stream), *recorder); // Synchronize the network SAFE_API_CALL(graph->sync(), *recorder); } CUDA_CALL(cudaStreamSynchronize(stream), *recorder); if (!isProfileRun) { safeLogInfo(*recorder, "Warmup completed."); safeLogInfo(*recorder, ""); // empty line safeLogInfo(*recorder, "=== Trace details ==="); } // Create cuda events for profiling cudaEvent_t startEvent, endEvent; CUDA_CALL(cudaEventCreateWithFlags(&startEvent, cudaEventFlags), *recorder); CUDA_CALL(cudaEventCreateWithFlags(&endEvent, cudaEventFlags), *recorder); cudaEvent_t syncEvent; CUDA_CALL(cudaEventCreateWithFlags(&syncEvent, cudaEventDisableTiming), *recorder); // Do inference auto const nbAvgRuns = args.avgRuns; auto const nbIterations = args.iterations; // GPU, host and enqueue times TimingMetrics totalTimes; using floatDurationMS = std::chrono::duration; floatDurationMS const maxDurationMs = floatDurationMS(args.duration * 1000); floatDurationMS durationMs{0}; for (int32_t i = 0; i < nbIterations || durationMs.count() < maxDurationMs.count(); i++) { TrtCudaGraphSafe cudaGraph; float totalGpuTime{0.F}; float totalHostTime{0.F}; float totalEnqueueTime{0.F}; if (args.useCudaGraph && !isProfileRun) { if (!graphCapture(stream, cudaGraph, graph, *recorder)) { safeLogError(*recorder, "Failed to capture graph."); return false; } } for (int32_t j = 0; j < nbAvgRuns; j++) { auto const startTime = std::chrono::high_resolution_clock::now(); if (isProfileRun) { if (graph->executeAsync(stream) != ErrorCode::kSUCCESS) { safeLogError(*recorder, "Failed to run executeAsync during average runs."); return false; } SAFE_API_CALL(graph->sync(), *recorder); auto const endTime = std::chrono::high_resolution_clock::now(); durationMs += floatDurationMS(endTime - startTime); continue; } CUDA_CHECK(delayStream(stream, args.sleep)); CUDA_CALL(cudaEventRecord(startEvent, stream), *recorder); CUDA_CALL(cudaStreamWaitEvent(stream, startEvent, 0), *recorder); if (args.useCudaGraph) { if (!cudaGraph.launch(stream)) { safeLogError(*recorder, "Failed to launch graph."); return false; } } else { if (graph->executeAsync(stream) != ErrorCode::kSUCCESS) { safeLogError(*recorder, "Failed to run executeAsync during average runs."); return false; } } CUDA_CALL(cudaEventRecord(syncEvent, stream), *recorder); CUDA_CALL(cudaStreamWaitEvent(stream, syncEvent, 0), *recorder); auto const enqueueEndTime = std::chrono::high_resolution_clock::now(); CUDA_CALL(cudaEventRecord(endEvent, stream), *recorder); CUDA_CALL(cudaEventSynchronize(endEvent), *recorder); if (i == 0 && j == 0) { CUDA_CALL(cudaEventElapsedTime(&absStartTime, gpuStart, startEvent), *recorder); } if ((i == nbIterations - 1) && (j == nbAvgRuns - 1)) { CUDA_CALL(cudaEventElapsedTime(&absEndTime, gpuStart, endEvent), *recorder); } auto const endTime = std::chrono::high_resolution_clock::now(); float gpuTime{0.F}; CUDA_CALL(cudaEventElapsedTime(&gpuTime, startEvent, endEvent), *recorder); auto const enqueueTime = std::chrono::duration(enqueueEndTime - startTime).count(); auto const hostTime = std::chrono::duration(endTime - startTime).count(); durationMs += floatDurationMS(hostTime); totalGpuTime += gpuTime; totalHostTime += hostTime; totalEnqueueTime += enqueueTime; // Mimic waiting for user input data (default = 0) std::this_thread::sleep_for(std::chrono::duration(args.idle)); } if (isProfileRun) { continue; } auto const avgGpuTime = totalGpuTime / nbAvgRuns; auto const avgHostTime = totalHostTime / nbAvgRuns; auto const avgEnqueueTime = totalEnqueueTime / nbAvgRuns; totalTimes.push_back({avgGpuTime, avgHostTime, avgEnqueueTime}); std::stringstream ss; ss << "Average over " << nbAvgRuns << " runs - GPU latency: " << avgGpuTime << " ms - Host latency: " << avgHostTime << " ms (enqueue " << avgEnqueueTime << " ms)"; safeLogInfo(*recorder, ss.str()); } if (!isProfileRun) { std::stringstream ss; // Sort GPU times std::sort(totalTimes.begin(), totalTimes.end(), [](TimingMetric const& a, TimingMetric const& b) { return a[0] < b[0]; }); auto const gpuTimeResult = getSafePerformanceResult(totalTimes, 0, args.percentile); auto const hostTimeResult = getSafePerformanceResult(totalTimes, 1, args.percentile); auto const enqueueTimeResult = getSafePerformanceResult(totalTimes, 2, args.percentile); auto const totalWallTime = absEndTime - absStartTime; // Print final profiling result safeLogInfo(*recorder, ""); // empty line safeLogInfo(*recorder, "=== Performance summary ==="); ss << "Total throughput: " << nbAvgRuns * nbIterations / totalWallTime * 1000 << " qps"; safeLogInfo(*recorder, ss.str()); ss.str(""); ss << "Host Time: min = " << hostTimeResult.min << " ms, max = " << hostTimeResult.max << " ms, mean = " << hostTimeResult.mean << " ms, median = " << hostTimeResult.median << " ms," << " percentile(" << args.percentile << "%) = " << hostTimeResult.percentile << " ms"; safeLogInfo(*recorder, ss.str()); ss.str(""); ss << "Enqueue Time: min = " << enqueueTimeResult.min << " ms, max = " << enqueueTimeResult.max << " ms, mean = " << enqueueTimeResult.mean << " ms, median = " << enqueueTimeResult.median << " ms," << " percentile(" << args.percentile << "%) = " << enqueueTimeResult.percentile << " ms"; safeLogInfo(*recorder, ss.str()); ss.str(""); ss << "GPU Compute Time: min = " << gpuTimeResult.min << " ms, max = " << gpuTimeResult.max << " ms, mean = " << gpuTimeResult.mean << " ms, median = " << gpuTimeResult.median << " ms," << " percentile(" << args.percentile << "%) = " << gpuTimeResult.percentile << " ms"; safeLogInfo(*recorder, ss.str()); ss.str(""); // Report warnings if the GPU Compute Time is unstable. constexpr float kUNSTABLE_PERF_REPORTING_THRESHOLD{1.0F}; if (gpuTimeResult.coeffVar > kUNSTABLE_PERF_REPORTING_THRESHOLD) { ss << "* GPU compute time is unstable, with coefficient of variance = " << gpuTimeResult.coeffVar << "%."; safeLogWarning(*recorder, ss.str()); ss.str(""); } } // Destroy cuda events CUDA_CALL(cudaEventDestroy(startEvent), *recorder); CUDA_CALL(cudaEventDestroy(endEvent), *recorder); CUDA_CALL(cudaEventDestroy(syncEvent), *recorder); CUDA_CALL(cudaEventDestroy(gpuStart), *recorder); CUDA_CALL(cudaEventDestroy(inputConsumedEvent), *recorder); // Destroy main execution cuda stream CUDA_CALL(cudaStreamDestroy(stream), *recorder); // Buffers are automatically freed by ScopedSafeMemory destructors return true; } //! //! \brief Runs the TensorRT inference engine for this sample. //! //! \details This function is the main execution function of the sample. It loads the engine, allocates //! the buffer, executes the engine and reports the performance. //! //! \param isProfileRun If true, the function will launch a separate profile run and dump safe profiling data. //! bool doInference(SafeExecArgs const& args, std::chrono::high_resolution_clock::time_point const& initStartTime, bool isProfileRun = false) { int32_t numThreads = args.threads; if (isProfileRun) { numThreads = 1; } // Configure recorder(s) std::vector> recorders(numThreads); for (int32_t k = 0; k < numThreads; ++k) { auto severity = nvinfer2::safe::Severity::kINFO; if (args.debug) { severity = nvinfer2::safe::Severity::kDEBUG; } else if (args.verbose) severity = nvinfer2::safe::Severity::kVERBOSE; recorders[k] = std::make_unique(severity, k); } // Load safe engine blob std::vector blob{loadEngine(args.engineFile)}; if (blob.data() == nullptr) { safeLogError(*recorders[0], "Engine blob is empty."); return false; } if (!isProfileRun) { // Register plugins only on the first run. doInference() is called twice (normal run then profile run); // registering again would assert with "existing plugins" error. registerSafetyPlugins(*gSafeRecorder, args.pluginLibraries); auto const initEndTime = std::chrono::high_resolution_clock::now(); auto const initTime = std::chrono::duration(initEndTime - initStartTime).count(); safeLogInfo(*recorders[0], "TensorRT init time is " + std::to_string(initTime) + " ms."); } else { safeLogInfo(*recorders[0], "Starting separate safe profiling run."); } // Configure executor(s) std::vector graphs(numThreads); std::vector scratchs(numThreads); SAFE_API_CALL(nvinfer2::safe::createTRTGraph(graphs[0], blob.data(), blob.size(), *recorders[0], !args.useScratchMemory, &nvinfer2::safe::getSafeMemAllocator()), *recorders[0]); SAFE_API_CALL(graphs[0]->setIOProfile(args.ioProfile), *recorders[0]); for (int32_t k = 1; k < numThreads; ++k) { SAFE_API_CALL(graphs[0]->clone(graphs[k], *recorders[k]), *recorders[0]); SAFE_API_CALL(graphs[k]->setIOProfile(args.ioProfile), *recorders[k]); } // Configure scratch memory if (args.useScratchMemory) { size_t scratchSize = 0; SAFE_API_CALL(graphs[0]->getScratchMemorySize(scratchSize), *recorders[0]); for (int32_t k = 0; k < numThreads; ++k) { CUDA_CALL(cudaMalloc(&scratchs[k], scratchSize), *recorders[k]); SAFE_API_CALL(graphs[k]->setScratchMemory(scratchs[k]), *recorders[k]); } } // Run the graphs in independent threads std::vector> futureResults; for (int32_t k = 0; k < numThreads; ++k) { // launch thread async futureResults.emplace_back( std::async(std::launch::async, task, args, graphs[k], recorders[k].get(), isProfileRun)); } for (auto& future : futureResults) { if (!future.get()) { safeLogError(*recorders[0], "Inference failed."); return false; } } if (args.useScratchMemory) { for (int32_t k = 0; k < numThreads; ++k) { CUDA_CALL(cudaFree(scratchs[k]), *recorders[k]); scratchs[k] = nullptr; SAFE_API_CALL(graphs[k]->setScratchMemory(nullptr), *recorders[k]); } } for (int32_t k = 0; k < numThreads; ++k) { SAFE_API_CALL(nvinfer2::safe::destroyTRTGraph(graphs[k]), *recorders[k]); graphs[k] = nullptr; } return true; } //! //! \brief Set device and print device information //! bool setDevice(SafeExecArgs const& args) { CUDA_CHECK(cudaSetDevice(args.device)); int32_t numSMs{0}; int32_t memoryBusWidth{0}; int32_t major{0}; int32_t minor{0}; CUDA_CHECK(cudaDeviceGetAttribute(&numSMs, cudaDevAttrMultiProcessorCount, args.device)); CUDA_CHECK(cudaDeviceGetAttribute(&memoryBusWidth, cudaDevAttrGlobalMemoryBusWidth, args.device)); // We print the actual SM in use. CUDA_CHECK(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, args.device)); CUDA_CHECK(cudaDeviceGetAttribute(&minor, cudaDevAttrComputeCapabilityMinor, args.device)); safeLogInfo(*gSafeRecorder, "Running on CUDA device number: " + std::to_string(args.device) + " (" + std::to_string(numSMs) + " SMs, " + std::to_string(memoryBusWidth) + " bits, Compute Capability " + std::to_string(major) + "." + std::to_string(minor) + ")"); return true; } int32_t main(int32_t argc, char** argv) { reportTestStart("TensorRT.trtexec_safe", argc, argv); safetyCompliance::setPromgrAbility(); TestResult result = TestResult::kPASSED; // CUDA initialization int32_t currentDevice = 0; if (cudaGetDevice(¤tDevice) != cudaSuccess) { safeLogError(*gSafeRecorder, "CUDA initialization failed!"); return EXIT_FAILURE; } SafeExecArgs args; auto const initStartTime = std::chrono::high_resolution_clock::now(); if (!parseSafeExecArgs(args, argc, argv)) { printHelpInfo(); return EXIT_FAILURE; } if (args.help) { printHelpInfo(); return EXIT_SUCCESS; } if (!setDevice(args)) { result = TestResult::kFAILED; } else { try { if (!doInference(args, initStartTime)) { result = TestResult::kFAILED; } } catch (std::runtime_error& e) { safeLogError(*gSafeRecorder, e.what()); result = TestResult::kFAILED; } // Separate profile run (always enabled) setenv("ENABLE_SAFE_PROFILING", "1", 1); try { if (!doInference(args, initStartTime, /* isProfileRun = */ true)) { result = TestResult::kFAILED; } } catch (std::runtime_error& e) { safeLogError(*gSafeRecorder, e.what()); result = TestResult::kFAILED; } unsetenv("ENABLE_SAFE_PROFILING"); } reportTestResult("TensorRT.trtexec_safe", result, argc, argv); return EXIT_SUCCESS; }