/* * 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. */ //! \file sampleSafeMNISTInfer.cpp //! \brief This file contains the implementation of the MNIST sample. //! //! It uses the prebuilt TensorRT engine to run inference on an input image of a digit. //! It can be run with the following command line: //! Command: ./sample_mnist_safe_infer #include "NvInferSafeRuntime.h" #include "safeCommon.h" #include "safeErrorRecorder.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace samplesSafeCommon; namespace { //! //! \brief Locate path to file by its filename. Will walk back MAX_DEPTH dirs from CWD to check for such a file path. //! std::string locateFile(std::string const& fileName, nvinfer2::safe::ISafeRecorder& recorder) { constexpr uint32_t MAX_DEPTH{10U}; std::array const dirPatterns = {std::string{"data/samples/mnist/"}, std::string{"data/mnist/"}}; std::string foundFile{}; for (auto const& dir : dirPatterns) { std::string file{dir + fileName}; bool found{false}; for (uint32_t i = 0U; i < MAX_DEPTH; i++) { std::ifstream checkFile(file); found = checkFile.is_open(); if (found) { break; } file = "../" + file; // Try again in parent dir. } if (found) { foundFile = file; break; } } if (foundFile.empty()) { safeLogError(recorder, "Could not find " + fileName + " in data/samples/mnist/ or data/mnist."); safeLogError(recorder, "&&&& FAILED"); exit(EXIT_FAILURE); } return foundFile; } //! //! \brief Reads the input data, preprocesses, and stores the result in a managed buffer. //! bool processInput(void* input, int32_t const inputFileIdx, nvinfer2::safe::ISafeRecorder& recorder) { std::stringstream ss; constexpr int32_t kINPUT_H{28}; constexpr int32_t kINPUT_W{28}; // Read the digit file according to the inputFileIdx. std::vector fileData(kINPUT_H * kINPUT_W); readPGMFile(locateFile(std::to_string(inputFileIdx) + ".pgm", recorder), fileData.data(), kINPUT_H, kINPUT_W); // Print ASCII representation of digit. ss << "Input:\n"; for (int32_t i = 0; i < kINPUT_H * kINPUT_W; i++) { ss << (" .:-=+*#%@"[fileData[i] / 26U]) << (((i + 1) % kINPUT_W) ? "" : "\n"); } safeLogInfo(recorder, ss.str()); float* hostInputBuffer = static_cast(input); std::copy(fileData.begin(), fileData.end(), hostInputBuffer); // Normalize to 0-1 with background at 0 std::transform(hostInputBuffer, hostInputBuffer + kINPUT_H * kINPUT_W, hostInputBuffer, [](float v) -> float { return 1.0f - v / 255.0f; }); return true; } //! //! \brief Verifies that the output is correct and prints it. //! bool verifyOutput(void* output, int32_t groundTruthDigit, nvinfer2::safe::ISafeRecorder& recorder) { float* prob = static_cast(output); // Print histogram of the output distribution. safeLogInfo(recorder, "Output:"); float val{0.0f}; int32_t idx{0}; constexpr int32_t kDIGITS{10}; // Calculate Softmax float sum{0.0f}; for (int32_t i = 0; i < kDIGITS; ++i) { prob[i] = exp(prob[i]); sum += prob[i]; } for (int32_t i = 0; i < kDIGITS; ++i) { std::stringstream ss; prob[i] /= sum; if (val < prob[i]) { val = prob[i]; idx = i; } ss << " Prob " << i << " " << std::fixed << std::setw(5) << std::setprecision(4) << prob[i] << " Class " << i << ": " << std::string(int32_t(std::floor(prob[i] * 10 + 0.5f)), '*'); safeLogInfo(recorder, ss.str()); } return (idx == groundTruthDigit && val > 0.9f); } //! //! \brief Loads the enginePlanFile from engineFile and returns it. //! std::vector loadEnginePlanFile(std::string const& engineFile, int& size, nvinfer2::safe::ISafeRecorder& recorder) { std::string const& filename = engineFile; std::vector gieModelStream; std::ifstream file(filename, std::ios::binary); if (!file.good()) { safeLogError(recorder, "Could not open input engine file or file is empty. File name: " + filename); return {}; } file.seekg(0, std::ifstream::end); size = file.tellg(); file.seekg(0, std::ifstream::beg); gieModelStream.resize(size); file.read(gieModelStream.data(), size); file.close(); return gieModelStream; } //! //! \brief Returns a random digit between 0 and 9 //! int32_t getRandomDigit() { std::random_device rd; std::default_random_engine generator{rd()}; std::uniform_int_distribution distribution(0, 9); return distribution(generator); } //! //! \brief Structure representing memory allocation for CUDA //! struct CudaMemory { void* hostPtr = nullptr; void* devicePtr = nullptr; size_t size = 0; }; //! //! \brief Do inference //! void doInferenceThread(nvinfer2::safe::ITRTGraph* graph, int8_t& ret_status, nvinfer2::safe::ISafeRecorder* recorder) { // Initialize to success; will be set to 0 on any error. ret_status = 1; int64_t nbIOs{}; SAFE_API_CALL(graph->getNbIOTensors(nbIOs), *recorder); // This sample only has one input and one output. SAFE_ASSERT(nbIOs == 2); CudaMemory inputCudaMemory; CudaMemory outputCudaMemory; // 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); // Pick a random digit to try to infer. int32_t digit = getRandomDigit(); // Iterate through all input/output tensors for (int64_t i = 0; i < nbIOs; ++i) { // Get the tensor name for the current I/O tensor char const* tensorName; SAFE_API_CALL(graph->getIOTensorName(tensorName, i), *recorder); // Get tensor descriptor which contains metadata like size and I/O mode nvinfer2::safe::TensorDescriptor desc; SAFE_API_CALL(graph->getIOTensorDescriptor(desc, tensorName), *recorder); // Allocate device and host memory for this tensor void* deviceBuf = nullptr; void* hostBuf = nullptr; CUDA_CALL(cudaMalloc(&deviceBuf, desc.sizeInBytes), *recorder); CUDA_CHECK(cudaHostAlloc(&hostBuf, desc.sizeInBytes, cudaHostAllocDefault)); if (desc.ioMode == TensorIOMode::kINPUT) { // Read the input data into the managed buffers. processInput(hostBuf, digit, *recorder); // Asynchronously copy data from host input buffers to device input buffers. CUDA_CHECK(cudaMemcpyAsync(deviceBuf, hostBuf, desc.sizeInBytes, cudaMemcpyHostToDevice, stream)); inputCudaMemory = {hostBuf, deviceBuf, desc.sizeInBytes}; } else if (desc.ioMode == TensorIOMode::kOUTPUT) { CUDA_CALL(cudaMemsetAsync(deviceBuf, 0, desc.sizeInBytes, stream), *recorder); outputCudaMemory = {hostBuf, deviceBuf, desc.sizeInBytes}; } else { safeLogError(*recorder, "Unexpected tensor IO mode"); ret_status = 0; } SAFE_ASSERT(desc.dataType == DataType::kFLOAT); // Create a typed array for the tensor nvinfer2::safe::TypedArray tensor = nvinfer2::safe::TypedArray(static_cast(deviceBuf), desc.sizeInBytes); SAFE_API_CALL(graph->setIOTensorAddress(tensorName, tensor), *recorder); } cudaEvent_t inputConsumedEvent; cudaEventCreate(&inputConsumedEvent); SAFE_API_CALL(graph->setInputConsumedEvent(inputConsumedEvent), *recorder); // Run the graph SAFE_API_CALL(graph->executeAsync(stream), *recorder); cudaEvent_t retrievedEvent; SAFE_API_CALL(graph->getInputConsumedEvent(retrievedEvent), *recorder); SAFE_ASSERT(retrievedEvent != nullptr); cudaEventSynchronize(retrievedEvent); // Synchronize the network SAFE_API_CALL(graph->sync(), *recorder); // Asynchronously copy data from device output buffers to host output buffers. CUDA_CHECK(cudaMemcpyAsync( outputCudaMemory.hostPtr, outputCudaMemory.devicePtr, outputCudaMemory.size, cudaMemcpyDeviceToHost, stream)); // Wait for the work in the stream to complete. CUDA_CHECK(cudaStreamSynchronize(stream)); // Check and print the output of the inference. if (!verifyOutput(outputCudaMemory.hostPtr, digit, *recorder)) { safeLogError(*recorder, "Failed to verify output"); ret_status = 0; } // Release stream and buffers. CUDA_CHECK(cudaStreamDestroy(stream)); CUDA_CHECK(cudaFreeHost(inputCudaMemory.hostPtr)); CUDA_CHECK(cudaFreeHost(outputCudaMemory.hostPtr)); CUDA_CHECK(cudaFree(inputCudaMemory.devicePtr)); CUDA_CHECK(cudaFree(outputCudaMemory.devicePtr)); } //! //! \brief The SampleSafeMNISTInferArgs struct stores the additional arguments required by the sample //! struct SampleSafeMNISTInferArgs { std::string engineFileName{"safe_mnist.engine"}; int32_t threads{1}; bool help{false}; }; //! //! \brief Runs the TensorRT inference engine for this sample. //! //! \details This function is the main execution function of the sample. It allocates //! the buffer, sets inputs, executes the engine, and verifies the output. //! bool doInference(SampleSafeMNISTInferArgs const& args) { int32_t const nbThreads = args.threads; std::vector ret_status(nbThreads); std::vector> recorders(nbThreads); for (int32_t i = 0; i < nbThreads; ++i) { recorders[i] = std::make_unique(nvinfer2::safe::Severity::kINFO, i); } // Load safe engine blob int32_t engineFileSize = 0; auto gieModelStream = loadEnginePlanFile(args.engineFileName, engineFileSize, *recorders[0]); SAFE_ASSERT(engineFileSize != 0); // Configure executor(s) std::vector graphs(nbThreads); SAFE_API_CALL(nvinfer2::safe::createTRTGraph(graphs[0], gieModelStream.data(), engineFileSize, *recorders[0], true), *recorders[0]); for (int32_t i = 1; i < nbThreads; ++i) { SAFE_API_CALL(graphs[0]->clone(graphs[i], *recorders[i]), *recorders[0]); } // Run the graphs in independent threads std::vector threads(nbThreads); for (int32_t i = 0; i < nbThreads; ++i) { threads[i] = std::thread{doInferenceThread, graphs[i], std::ref(ret_status[i]), recorders[i].get()}; } for (int32_t i = 0; i < nbThreads; ++i) { threads[i].join(); if (!ret_status[i]) { return false; } } for (int32_t i = 0; i < nbThreads; ++i) { SAFE_API_CALL(nvinfer2::safe::destroyTRTGraph(graphs[i]), *recorders[i]); graphs[i] = nullptr; } return true; } //! //! \brief This function parses arguments specific to the sample //! bool parseSampleSafeMNISTInferArgs(SampleSafeMNISTInferArgs& args, int32_t argc, char* argv[]) { for (int32_t i = 1; i < argc; ++i) { std::string const arg = argv[i]; if (auto const value = parseString(arg, "loadEngine")) { args.engineFileName = *value; } else if (auto const value = parseString(arg, "threads")) { args.threads = std::stoi(*value); if (args.threads <= 0) { SAFE_LOG << "Number of threads must be > 0, got: " << arg << "\n"; return false; } } else if (parseBool(arg, "help", 'h')) { args.help = true; } else { SAFE_LOG << "Invalid Argument: " << arg << "\n"; return false; } } return true; } //! //! \brief Prints the help information for running this sample. //! void printHelpInfo() { SampleSafeMNISTInferArgs const defArgs{}; std::cout << R"(Usage: sample_mnist_safe_infer [options] Options: --help, -h Print this message and exit. --loadEngine=FILE Load serialized engine from FILE (default = )" << defArgs.engineFileName << R"(). --threads=N Run inference in N threads concurrently (default = )" << defArgs.threads << R"(). )"; } } // namespace int32_t main(int32_t argc, char** argv) { safetyCompliance::setPromgrAbility(); SampleSafeMNISTInferArgs args; bool argsOK = parseSampleSafeMNISTInferArgs(args, argc, argv); if (!argsOK) { printHelpInfo(); return EXIT_FAILURE; } if (args.help) { printHelpInfo(); return EXIT_SUCCESS; } // Initialize SafeCuda before any other Cuda APIs are called. This may be skipped if createInferRuntime() is called // first as per DEEPLRN_RES_116 safetyCompliance::initSafeCuda(); if (!isSmSafe()) { SAFE_LOG << "Skip safe mode test on unsupported platforms." << std::endl; return EXIT_SUCCESS; } TestResult result = doInference(args) ? TestResult::kPASSED : TestResult::kFAILED; reportTestResult("TensorRT.sample_mnist_safe_infer", result, argc, argv); return EXIT_SUCCESS; }