/* * SPDX-FileCopyrightText: Copyright (c) 1993-2025 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. */ //! //! sampleDynamicReshape.cpp //! This file contains the implementation of the dynamic reshape MNIST sample. It creates a network //! using the MNIST ONNX model, and uses a second engine to resize inputs to the shape the model //! expects. //! It can be run with the following command: //! Command: ./sample_dynamic_reshape [-h or --help [-d=/path/to/data/dir or --datadir=/path/to/data/dir] //! // Define TRT entrypoints used in common code #define DEFINE_TRT_ENTRYPOINTS 1 #include "BatchStream.h" #include "argsParser.h" #include "buffers.h" #include "common.h" #include "logger.h" #include "parserOnnxConfig.h" #include "NvInfer.h" #include #include using namespace nvinfer1; const std::string gSampleName = "TensorRT.sample_dynamic_reshape"; //! \brief The SampleDynamicReshape class implements the dynamic reshape sample. //! //! \details This class builds one engine that resizes a given input to the correct size, and a //! second engine based on an ONNX MNIST model that generates a prediction. //! class SampleDynamicReshape { public: SampleDynamicReshape(const samplesCommon::OnnxSampleParams& params) : mParams(params) { } //! //! \brief Builds both engines. //! bool build(); //! //! \brief Prepares the model for inference by creating execution contexts and allocating buffers. //! bool prepare(); //! //! \brief Runs inference using TensorRT on a random image. //! bool infer(); private: [[nodiscard]] bool buildPreprocessorEngine( nvinfer1::IBuilder& builder, nvinfer1::IRuntime& runtime, cudaStream_t profileStream); [[nodiscard]] bool buildPredictionEngine( nvinfer1::IBuilder& builder, nvinfer1::IRuntime& runtime, cudaStream_t profileStream); [[nodiscard]] Dims loadPGMFile(const std::string& fileName); [[nodiscard]] bool validateOutput(int digit); samplesCommon::OnnxSampleParams mParams; //!< The parameters for the sample. nvinfer1::Dims mPredictionInputDims; //!< The dimensions of the input of the MNIST model. nvinfer1::Dims mPredictionOutputDims; //!< The dimensions of the output of the MNIST model. std::unique_ptr mRuntime{nullptr}; // Engine plan files used for inference. One for resizing inputs, another for prediction. std::unique_ptr mPreprocessorEngine{nullptr}, mPredictionEngine{nullptr}; std::unique_ptr mPreprocessorContext{nullptr}, mPredictionContext{nullptr}; samplesCommon::ManagedBuffer mInput{}; //!< Host and device buffers for the input. samplesCommon::DeviceBuffer mPredictionInput{}; //!< Device buffer for the output of the preprocessor, i.e. the //!< input to the prediction model. samplesCommon::ManagedBuffer mOutput{}; //!< Host buffer for the output }; //! //! \brief Builds the two engines required for inference. //! //! \details This function creates one TensorRT engine for resizing inputs to the correct sizes, //! then creates a TensorRT network by parsing the ONNX model and builds //! an engine that will be used to run inference (mPredictionEngine). //! //! \return false if error in build preprocessor or predict engine. //! bool SampleDynamicReshape::build() { auto builder = std::unique_ptr(nvinfer1::createInferBuilder(sample::gLogger.getTRTLogger())); if (!builder) { sample::gLogError << "Create inference builder failed." << std::endl; return false; } mRuntime = std::unique_ptr(nvinfer1::createInferRuntime(sample::gLogger.getTRTLogger())); if (!mRuntime) { sample::gLogError << "Runtime object creation failed." << std::endl; return false; } // This function will also set mPredictionInputDims and mPredictionOutputDims, // so it needs to be called before building the preprocessor. try { // CUDA stream used for profiling by the builder. auto profileStream = samplesCommon::makeCudaStream(); if (!profileStream) { return false; } bool result = buildPredictionEngine(*builder, *mRuntime, *profileStream) && buildPreprocessorEngine(*builder, *mRuntime, *profileStream); return result; } catch (std::runtime_error& e) { sample::gLogError << e.what() << std::endl; return false; } } //! //! \brief Builds an engine for preprocessing (mPreprocessorEngine). //! //! \return false if error in build preprocessor engine. //! bool SampleDynamicReshape::buildPreprocessorEngine( nvinfer1::IBuilder& builder, nvinfer1::IRuntime& runtime, cudaStream_t profileStream) { // Create the preprocessor engine using a network that supports full dimensions (createNetworkV2). auto preprocessorNetwork = std::unique_ptr( builder.createNetworkV2(1U << static_cast(NetworkDefinitionCreationFlag::kSTRONGLY_TYPED))); if (!preprocessorNetwork) { sample::gLogError << "Create network failed." << std::endl; return false; } // Reshape a dynamically shaped input to the size expected by the model, (1, 1, 28, 28). auto input = preprocessorNetwork->addInput("input", nvinfer1::DataType::kFLOAT, Dims4{-1, 1, -1, -1}); auto resizeLayer = preprocessorNetwork->addResize(*input); resizeLayer->setOutputDimensions(mPredictionInputDims); preprocessorNetwork->markOutput(*resizeLayer->getOutput(0)); // Finally, configure and build the preprocessor engine. auto preprocessorConfig = std::unique_ptr{builder.createBuilderConfig()}; if (!preprocessorConfig) { sample::gLogError << "Create builder config failed." << std::endl; return false; } // Create an optimization profile so that we can specify a range of input dimensions. auto profile = builder.createOptimizationProfile(); // This profile will be valid for all images whose size falls in the range of [(1, 1, 1, 1), (1, 1, 56, 56)] // but TensorRT will optimize for (1, 1, 28, 28) // We do not need to check the return of setDimension and addOptimizationProfile here as all dims are explicitly set profile->setDimensions(input->getName(), OptProfileSelector::kMIN, Dims4{1, 1, 1, 1}); profile->setDimensions(input->getName(), OptProfileSelector::kOPT, Dims4{1, 1, 28, 28}); profile->setDimensions(input->getName(), OptProfileSelector::kMAX, Dims4{1, 1, 56, 56}); preprocessorConfig->addOptimizationProfile(profile); std::unique_ptr timingCache{}; // Load timing cache if (!mParams.timingCacheFile.empty()) { timingCache = samplesCommon::buildTimingCacheFromFile( sample::gLogger.getTRTLogger(), *preprocessorConfig, mParams.timingCacheFile); } auto preprocessorPlan = std::unique_ptr( builder.buildSerializedNetwork(*preprocessorNetwork, *preprocessorConfig)); if (!preprocessorPlan) { sample::gLogError << "Preprocessor serialized engine build failed." << std::endl; return false; } if (timingCache != nullptr && !mParams.timingCacheFile.empty()) { samplesCommon::updateTimingCacheFile( sample::gLogger.getTRTLogger(), mParams.timingCacheFile, timingCache.get(), builder); } mPreprocessorEngine = std::unique_ptr( runtime.deserializeCudaEngine(preprocessorPlan->data(), preprocessorPlan->size())); if (!mPreprocessorEngine) { sample::gLogError << "Preprocessor engine deserialization failed." << std::endl; return false; } auto const tensorName = mPreprocessorEngine->getIOTensorName(0); sample::gLogInfo << "Profile dimensions in preprocessor engine:" << std::endl; sample::gLogInfo << " Minimum = " << mPreprocessorEngine->getProfileShape(tensorName, 0, OptProfileSelector::kMIN) << std::endl; sample::gLogInfo << " Optimum = " << mPreprocessorEngine->getProfileShape(tensorName, 0, OptProfileSelector::kOPT) << std::endl; sample::gLogInfo << " Maximum = " << mPreprocessorEngine->getProfileShape(tensorName, 0, OptProfileSelector::kMAX) << std::endl; return true; } //! //! \brief Builds an engine for prediction (mPredictionEngine). //! //! \details This function builds an engine for the MNIST model, and updates mPredictionInputDims and //! mPredictionOutputDims according to the dimensions specified by the model. The preprocessor reshapes inputs to //! mPredictionInputDims. //! //! \return false if error in build prediction engine. //! bool SampleDynamicReshape::buildPredictionEngine( nvinfer1::IBuilder& builder, nvinfer1::IRuntime& runtime, cudaStream_t profileStream) { // Create a network using the parser. auto network = std::unique_ptr( builder.createNetworkV2(1U << static_cast(NetworkDefinitionCreationFlag::kSTRONGLY_TYPED))); if (!network) { sample::gLogError << "Create network failed." << std::endl; return false; } auto const parser = std::unique_ptr(nvonnxparser::createParser(*network, sample::gLogger.getTRTLogger())); if (parser == nullptr) { throw std::runtime_error("Failed to create ONNX parser"); } bool parsingSuccess = parser->parseFromFile(samplesCommon::locateFile(mParams.onnxFileName, mParams.dataDirs).c_str(), static_cast(sample::gLogger.getReportableSeverity())); if (!parsingSuccess) { sample::gLogError << "Failed to parse model." << std::endl; return false; } // Attach a softmax layer to the end of the network. auto softmax = network->addSoftMax(*network->getOutput(0)); // Set softmax axis to 1 since network output has shape [1, 10] in full dims mode softmax->setAxes(1 << 1); network->unmarkOutput(*network->getOutput(0)); network->markOutput(*softmax->getOutput(0)); // Get information about the inputs/outputs directly from the model. mPredictionInputDims = network->getInput(0)->getDimensions(); mPredictionOutputDims = network->getOutput(0)->getDimensions(); // Create a builder config auto config = std::unique_ptr(builder.createBuilderConfig()); if (!config) { sample::gLogError << "Create builder config failed." << std::endl; return false; } config->setProfileStream(profileStream); // Build the prediction engine. std::unique_ptr timingCache{}; // Load timing cache if (!mParams.timingCacheFile.empty()) { timingCache = samplesCommon::buildTimingCacheFromFile(sample::gLogger.getTRTLogger(), *config, mParams.timingCacheFile); } // Build the prediction engine. auto predictionPlan = std::unique_ptr(builder.buildSerializedNetwork(*network, *config)); if (!predictionPlan) { sample::gLogError << "Prediction serialized engine build failed." << std::endl; return false; } if (timingCache != nullptr && !mParams.timingCacheFile.empty()) { samplesCommon::updateTimingCacheFile( sample::gLogger.getTRTLogger(), mParams.timingCacheFile, timingCache.get(), builder); } mPredictionEngine = std::unique_ptr( runtime.deserializeCudaEngine(predictionPlan->data(), predictionPlan->size())); if (!mPredictionEngine) { sample::gLogError << "Prediction engine deserialization failed." << std::endl; return false; } return true; } //! //! \brief Prepares the model for inference by creating an execution context and allocating buffers. //! //! \details This function sets up the sample for inference. This involves allocating buffers for the inputs and //! outputs, as well as creating TensorRT execution contexts for both engines. This only needs to be called a single //! time. //! //! \return false if error in build preprocessor or predict context. //! bool SampleDynamicReshape::prepare() { mPreprocessorContext = std::unique_ptr(mPreprocessorEngine->createExecutionContext()); if (!mPreprocessorContext) { sample::gLogError << "Preprocessor context build failed." << std::endl; return false; } mPredictionContext = std::unique_ptr(mPredictionEngine->createExecutionContext()); if (!mPredictionContext) { sample::gLogError << "Prediction context build failed." << std::endl; return false; } // Since input dimensions are not known ahead of time, we only allocate the output buffer and preprocessor output // buffer. mPredictionInput.resize(mPredictionInputDims); mOutput.hostBuffer.resize(mPredictionOutputDims); mOutput.deviceBuffer.resize(mPredictionOutputDims); return true; } //! //! \brief Runs inference for this sample //! //! \details This function is the main execution function of the sample. //! It runs inference for using a random image from the MNIST dataset as an input. //! bool SampleDynamicReshape::infer() { // Load a random PGM file into a host buffer, then copy to device. std::random_device rd{}; std::default_random_engine generator{rd()}; std::uniform_int_distribution digitDistribution{0, 9}; int digit = digitDistribution(generator); Dims inputDims = loadPGMFile(samplesCommon::locateFile(std::to_string(digit) + ".pgm", mParams.dataDirs)); mInput.deviceBuffer.resize(inputDims); CHECK(cudaMemcpy( mInput.deviceBuffer.data(), mInput.hostBuffer.data(), mInput.hostBuffer.nbBytes(), cudaMemcpyHostToDevice)); // Set the input size for the preprocessor CHECK_RETURN_W_MSG(mPreprocessorContext->setInputShape("input", inputDims), false, "Invalid binding dimensions."); // We can only run inference once all dynamic input shapes have been specified. if (!mPreprocessorContext->allInputDimensionsSpecified()) { return false; } // Run the preprocessor to resize the input to the correct shape std::vector preprocessorBindings = {mInput.deviceBuffer.data(), mPredictionInput.data()}; // For engines using full dims, we can use executeV2, which does not include a separate batch size parameter. bool status = mPreprocessorContext->executeV2(preprocessorBindings.data()); if (!status) { return false; } // Next, run the model to generate a prediction. std::vector predicitonBindings = {mPredictionInput.data(), mOutput.deviceBuffer.data()}; status = mPredictionContext->executeV2(predicitonBindings.data()); if (!status) { return false; } // Copy the outputs back to the host and verify the output. CHECK(cudaMemcpy(mOutput.hostBuffer.data(), mOutput.deviceBuffer.data(), mOutput.deviceBuffer.nbBytes(), cudaMemcpyDeviceToHost)); return validateOutput(digit); } //! //! \brief Loads a PGM file into mInput and returns the dimensions of the loaded image. //! //! \details This function loads the specified PGM file into the input host buffer. //! Dims SampleDynamicReshape::loadPGMFile(const std::string& fileName) { std::ifstream infile(fileName, std::ifstream::binary); ASSERT(infile.is_open() && "Attempting to read from a file that is not open."); std::string magic; int h, w, max; infile >> magic >> h >> w >> max; infile.seekg(1, infile.cur); Dims4 inputDims{1, 1, h, w}; size_t vol = samplesCommon::volume(inputDims); std::vector fileData(vol); infile.read(reinterpret_cast(fileData.data()), vol); // Print an ascii representation sample::gLogInfo << "Input:\n"; for (size_t i = 0; i < vol; i++) { sample::gLogInfo << (" .:-=+*#%@"[fileData[i] / 26]) << (((i + 1) % w) ? "" : "\n"); } sample::gLogInfo << std::endl; // Normalize and copy to the host buffer. mInput.hostBuffer.resize(inputDims); float* hostDataBuffer = static_cast(mInput.hostBuffer.data()); std::transform(fileData.begin(), fileData.end(), hostDataBuffer, [](uint8_t x) { return 1.0 - static_cast(x / 255.0); }); return inputDims; } //! //! \brief Checks whether the model prediction (in mOutput) is correct. //! bool SampleDynamicReshape::validateOutput(int digit) { const float* bufRaw = static_cast(mOutput.hostBuffer.data()); std::vector prob(bufRaw, bufRaw + mOutput.hostBuffer.size()); int curIndex{0}; for (const auto& elem : prob) { sample::gLogInfo << " Prob " << curIndex << " " << std::fixed << std::setw(5) << std::setprecision(4) << elem << " " << "Class " << curIndex << ": " << std::string(int(std::floor(elem * 10 + 0.5F)), '*') << std::endl; ++curIndex; } int predictedDigit = std::max_element(prob.begin(), prob.end()) - prob.begin(); return digit == predictedDigit; } //! //! \brief Initializes members of the params struct using the command line args //! samplesCommon::OnnxSampleParams initializeSampleParams(const samplesCommon::Args& args) { samplesCommon::OnnxSampleParams params; if (args.dataDirs.empty()) // Use default directories if user hasn't provided directory paths { params.dataDirs.push_back("data/mnist/"); params.dataDirs.push_back("data/samples/mnist/"); } else // Use the data directory provided by the user { params.dataDirs = args.dataDirs; } params.onnxFileName = "mnist.onnx"; params.inputTensorNames.push_back("Input3"); params.outputTensorNames.push_back("Plus214_Output_0"); params.timingCacheFile = args.timingCacheFile; return params; } //! //! \brief Prints the help information for running this sample //! void printHelpInfo() { std::cout << "Usage: ./sample_dynamic_reshape [-h or --help] [-d or --datadir=] " "[--timingCacheFile=]" << std::endl; std::cout << "--help, -h Display help information" << std::endl; std::cout << "--datadir Specify path to a data directory, overriding the default. This option can be used " "multiple times to add multiple directories. If no data directories are given, the default is to use " "(data/samples/mnist/, data/mnist/)" << std::endl; std::cout << "--timingCacheFile Specify path to a timing cache file. If it does not already exist, it will be " << "created." << std::endl; } int main(int argc, char** argv) { samplesCommon::Args args; bool argsOK = samplesCommon::parseArgs(args, argc, argv); if (!argsOK) { sample::gLogError << "Invalid arguments" << std::endl; printHelpInfo(); return EXIT_FAILURE; } if (args.help) { printHelpInfo(); return EXIT_SUCCESS; } auto sampleTest = sample::gLogger.defineTest(gSampleName, argc, argv); sample::gLogger.reportTestStart(sampleTest); SampleDynamicReshape sample{initializeSampleParams(args)}; if (!sample.build()) { return sample::gLogger.reportFail(sampleTest); } if (!sample.prepare()) { return sample::gLogger.reportFail(sampleTest); } if (!sample.infer()) { return sample::gLogger.reportFail(sampleTest); } return sample::gLogger.reportPass(sampleTest); }