#include "InferenceEngine.h" #include "Log.h" #include "picosha2.h" #include #include #include #include #include #include #include #include namespace { class Logger : public nvinfer1::ILogger { public: Logger( bool verbose ) : m_flags( UINT8_MAX ) { m_flags.set( static_cast( Severity::kVERBOSE ), verbose ); } void log( Severity severity, const char* msg ) noexcept override { if ( !m_flags.test( static_cast( severity ) ) ) return; switch ( severity ) { case Severity::kINFO: case Severity::kVERBOSE: LOG_INFO( msg ); break; case Severity::kWARNING: LOG_WARNING( msg ); break; case Severity::kERROR: case Severity::kINTERNAL_ERROR: LOG_ERROR( msg ); break; } } void log( cudaError_t error ) { if ( error == cudaError_t::cudaSuccess ) return; LOG_ERROR( "CUDA failed with error [ " + std::to_string( error ) + " : " + cudaGetErrorName( error ) + " ]: " + cudaGetErrorString( error ) ); } private: static constexpr uint8_t NB_FLAGS = static_cast( Severity::kVERBOSE ) + 1; std::bitset m_flags; }; Logger s_logger(false); bool FileExists( const std::string& filepath ) { return std::ifstream( filepath.c_str() ).good(); } std::string GetCudaDeviceName( int deviceID ) { int deviceCount = 0; cudaGetDeviceCount( &deviceCount ); if ( deviceID >= deviceCount ) return std::string(); cudaDeviceProp prop; cudaGetDeviceProperties( &prop, deviceID ); return std::string( prop.name ); } int SizeOfTensorDataType( nvinfer1::DataType dataType ) { int size = -1; switch ( dataType ) { case nvinfer1::DataType::kINT64: size = sizeof( uint64_t ); break; case nvinfer1::DataType::kINT32: case nvinfer1::DataType::kFLOAT: size = sizeof( uint32_t ); break; case nvinfer1::DataType::kHALF: case nvinfer1::DataType::kBF16: size = sizeof( uint16_t ); break; case nvinfer1::DataType::kFP8: case nvinfer1::DataType::kINT8: case nvinfer1::DataType::kBOOL: case nvinfer1::DataType::kUINT8: size = sizeof( uint8_t ); break; default: break; } return size; } } bool ConvertONNXToTRT( const Options& options, const std::string& onnxModelPath, std::string& trtFile, const std::string prefix, bool forceConvert ) { if ( !FileExists( onnxModelPath ) ) { LOG_ERROR( "Cannot find ONNX mode at path: " + onnxModelPath ); return false; } // Find a hash string identifying the onnx we're going to convert to // tensorrt, plus the gpu we're going to convert it for: std::ifstream file(onnxModelPath, std::ios::binary); std::string trtHash = picosha2::hash256_hex_string( std::istreambuf_iterator(file), std::istreambuf_iterator() ); // hash in the cuda device name and precision:: trtHash = picosha2::hash256_hex_string(trtHash + GetCudaDeviceName( options.deviceID )); trtHash = picosha2::hash256_hex_string(trtHash + std::to_string(int(options.precision))); // name of the tensorrt we're going to write to (same directory as the ONNX model): const auto filenamePos = onnxModelPath.find_last_of( '/' ) + 1; const auto onnxDir = onnxModelPath.substr(0, filenamePos); trtFile = onnxDir + prefix + onnxModelPath.substr( filenamePos, onnxModelPath.find_last_of( '.' ) - filenamePos ); trtFile += ".trt"; if (!forceConvert && FileExists( trtFile ) ) { std::ifstream file(trtFile, std::ios::binary); std::string hashInFile; hashInFile.resize(trtHash.size()); file.read(const_cast(hashInFile.c_str()), trtHash.size()); std::cerr << "hash value in file " << hashInFile << std::endl; std::cerr << "hash of onnx/gpu type " << trtHash << std::endl; if(hashInFile == trtHash) { return true; } LOG_INFO( "TRT file " + trtFile + " is out of date and needs to be regenerated. This could take a while..." ); } else { LOG_INFO( "TRT file " + trtFile + " not found. This could take a while to generate..." ); } // Create engine builder auto builder = std::unique_ptr( nvinfer1::createInferBuilder( s_logger ) ); if ( !builder ) { LOG_ERROR( "Could not create builder." ); return false; } // Create network const uint32_t explicitBatch = 1U << static_cast( nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH ); auto network = std::unique_ptr( builder->createNetworkV2( explicitBatch ) ); if ( !network ) { LOG_ERROR( "Could not create network." ); return false; } // Create a parser for reading the onnx file. auto parser = std::unique_ptr( nvonnxparser::createParser( *network, s_logger ) ); if ( !parser ) { LOG_ERROR( "Could not create ONNX parser." ); return false; } // Read and parse the ONNX file auto parsed = parser->parseFromFile( onnxModelPath.c_str(), 0 ); if ( !parsed ) { LOG_ERROR( "Unable to parse the ONNX model at: " + onnxModelPath ); return false; } // Ensure that all the inputs have the same batch size const int32_t inputCount = network->getNbInputs(); if ( inputCount < 1 ) { LOG_ERROR( "Model has no inputs." ); return false; } // Set dynamic axis info for inputs: auto profile = builder->createOptimizationProfile(); for ( int32_t i = 0; i < inputCount; ++i ) { auto t = network->getInput( i ); const auto name = t->getName(); auto dims = t->getDimensions(); bool hasDynamicAxes = false; auto dimsMin = dims; auto dimsOpt = dims; auto dimsMax = dims; for ( int32_t n = 0; n < dims.nbDims; ++n ) { if ( dims.d[n] == -1 ) { auto axis_sizes = options.defaultSizes; if (options.dynamic_axes_names.contains(t->getName())) { auto& dynamic_axis_names = options.dynamic_axes_names.at(t->getName()); if (!dynamic_axis_names.contains(n)) { LOG_ERROR("Tensor " + std::string(t->getName()) + " has a dynamic axes, but its name isn't specified in options.input_dynamic_axes"); return false; } auto axis_name = dynamic_axis_names.at(n); if (!options.dynamic_axes_sizes.contains(axis_name)) { LOG_ERROR("Named axis " + axis_name + " has no entry in options.dynamic_axes_sizes"); return false; } axis_sizes = options.dynamic_axes_sizes.at(axis_name); } hasDynamicAxes = true; dimsMin.d[n] = std::get<0>( axis_sizes ); dimsOpt.d[n] = std::get<1>( axis_sizes ); dimsMax.d[n] = std::get<2>( axis_sizes ); } } profile->setDimensions( name, nvinfer1::OptProfileSelector::kMIN, dimsMin ); profile->setDimensions( name, nvinfer1::OptProfileSelector::kOPT, dimsOpt ); profile->setDimensions( name, nvinfer1::OptProfileSelector::kMAX, dimsMax ); if ( t->isShapeTensor() ) { if ( !options.shape_tensor_sizes.contains( name ) ) { LOG_ERROR( "Tensor " + std::string( name ) + " controls the shape of an internal tensor and needs an entry in options.shape_tensor_sizes" ); return false; } auto &sizes = options.shape_tensor_sizes.at( name ); profile->setShapeValues( name, nvinfer1::OptProfileSelector::kMIN, std::get<0>( sizes ).data(), std::get<0>( sizes ).size()); profile->setShapeValues( name, nvinfer1::OptProfileSelector::kOPT, std::get<1>( sizes ).data(), std::get<1>( sizes ).size() ); profile->setShapeValues( name, nvinfer1::OptProfileSelector::kMAX, std::get<2>( sizes ).data(), std::get<2>( sizes ).size() ); } } auto config = std::unique_ptr( builder->createBuilderConfig() ); if ( !config ) { LOG_ERROR( "Could not create builder config." ); return false; } config->addOptimizationProfile( profile ); // Set the precision level if ( options.precision == Precision::FP16 ) { // Ensure the GPU supports FP16 inference if ( !builder->platformHasFastFp16() ) { LOG_ERROR( "GPU does not support FP16 precision." ); return false; } config->setFlag( nvinfer1::BuilderFlag::kFP16 ); } // CUDA stream used for profiling by the builder. cudaStream_t stream; s_logger.log( cudaStreamCreate( &stream ) ); config->setProfileStream( stream ); // Build the engine // If this call fails, it is suggested to increase the logger verbosity to kVERBOSE and try rebuilding the engine. // Doing so will provide you with more information on why exactly it is failing. auto serializedNetwork = builder->buildSerializedNetwork( *network, *config ); if ( !serializedNetwork ) { s_logger.log( cudaStreamDestroy( stream ) ); LOG_ERROR( "Failed to build the engine!" ); return false; } // Write the engine to disk std::ofstream outfile( trtFile, std::ofstream::binary ); // Write the onnx/gpu hash first: outfile.write( trtHash.c_str(), trtHash.size() ); // Write the engine: outfile.write( reinterpret_cast( serializedNetwork->data() ), serializedNetwork->size() ); LOG_INFO( "Saved TRT file to " + trtFile ); s_logger.log( cudaStreamDestroy( stream ) ); return true; } class TRTInferenceEngine::Impl { public: ~Impl() { Destroy(); } bool Initialize( const std::string& trtPath, int deviceIndex, const Options::AxisNames& axisNames ) { Destroy(); // Read the serialized model from disk std::ifstream file( trtPath, std::ios::binary | std::ios::ate ); std::streamsize size = file.tellg(); if (size < 0) { LOG_ERROR("Unable to read engine file!"); return false; } file.seekg( 0, std::ios::beg ); std::vector buffer( size ); if ( !file.read( buffer.data(), size ) ) { LOG_ERROR( "Unable to read engine file!" ); return false; } // Create runtime to deserialize the engine file. m_runtime = std::unique_ptr( nvinfer1::createInferRuntime( s_logger ) ); if ( !m_runtime ) { LOG_ERROR( "Unable to create a runtime inference model." ); return false; } // Set the device index if ( cudaSetDevice( deviceIndex ) != cudaError_t::cudaSuccess ) { int gpuCount; cudaGetDeviceCount( &gpuCount ); LOG_ERROR( "Unable to set GPU device index to " + std::to_string( deviceIndex ) + ". Current device has " + std::to_string( gpuCount ) + " CUDA - capable GPU(s)." ); return false; } // Create an engine, a representation of the optimized model. // skip the first 64 bytes when deserializing the engine file as they just contain a hash: m_engine = std::unique_ptr( m_runtime->deserializeCudaEngine( buffer.data() + 64, buffer.size() - 64 ) ); if ( !m_engine ) { LOG_ERROR("Unable to create engine."); return false; } // The execution context contains all of the state associated with a particular invocation m_context = std::unique_ptr( m_engine->createExecutionContext() ); if ( !m_context ) { LOG_ERROR("Unable to create execution context."); return false; } m_axis_names = axisNames; const uint32_t tensorCount = m_engine->getNbIOTensors(); for (int32_t i = 0; i < tensorCount; ++i) { const auto tensorName = m_engine->getIOTensorName(i); const auto tensorIOMode = m_engine->getTensorIOMode(tensorName); m_dataTypeSizes[tensorName] = SizeOfTensorDataType(m_engine->getTensorDataType(tensorName)); } return true; } bool InitInputs(const AxisSizes& axisSizes) { DestroyBuffers(); const uint32_t tensorCount = m_engine->getNbIOTensors(); for (int32_t i = 0; i < tensorCount; ++i) { const auto tensorName = m_engine->getIOTensorName(i); const auto tensorIOMode = m_engine->getTensorIOMode(tensorName); auto tensorShape = m_engine->getTensorShape(tensorName); const auto tensorDataType = m_engine->getTensorDataType(tensorName); for (int32_t n = 0; n < tensorShape.nbDims; ++n) { if (tensorShape.d[n] == -1) { int64_t batchSize = 1; if (m_axis_names.contains(tensorName)) { auto& dynamic_axis_names = m_axis_names.at(tensorName); if (!dynamic_axis_names.contains(n)) { LOG_ERROR("Tensor " + std::string(tensorName) + " has a dynamic axes, but its name isn't specified in axisNames"); return false; } auto axis_name = dynamic_axis_names.at(n); if (!axisSizes.contains(axis_name)) { LOG_ERROR("Named axis " + axis_name + " has no entry in axisSizes"); return false; } batchSize = axisSizes.at(axis_name); } else if (axisSizes.size() > 0) { batchSize = axisSizes.begin()->second; } tensorShape.d[n] = batchSize; if (tensorIOMode == nvinfer1::TensorIOMode::kINPUT) { auto batchMin = m_engine->getProfileShape(tensorName, 0, nvinfer1::OptProfileSelector::kMIN).d[n]; auto batchMax = m_engine->getProfileShape(tensorName, 0, nvinfer1::OptProfileSelector::kMAX).d[n]; if (batchSize < batchMin || batchSize > batchMax) { LOG_ERROR("Batch size " + std::to_string(batchSize) + " is outside of possible range [ " + std::to_string(batchMin) + ", " + std::to_string(batchMax) + "]."); return false; } } } } m_tensorShapes[tensorName] = tensorShape; static const std::string dataTypeStr[] = { "FLOAT", "HALF", "INT8", "INT32", "BOOL", "UINT8", "FP8", "BF16", "INT64" }; static const std::string ioTypeStr[] = { "NONE", "INPUT", "OUTPUT" }; std::string tensorInfo = ioTypeStr[int(tensorIOMode)] + " Tensor: " + std::string(tensorName) + " ["; size_t elementCount = 1; for (int32_t d = 0; d < tensorShape.nbDims; ++d) { if (d != 0) tensorInfo += ", "; tensorInfo += std::to_string(tensorShape.d[d]); if (elementCount > INT64_MAX / tensorShape.d[d]) { LOG_ERROR("Tensor too large: element count overflow"); return false; } elementCount *= tensorShape.d[d]; } tensorInfo += "]. " + std::to_string(elementCount) + " elements of type " + dataTypeStr[int(tensorDataType)]; LOG_INFO(tensorInfo); const auto sizeOfTensorData = SizeOfTensorDataType(tensorDataType); if (sizeOfTensorData == -1) { LOG_ERROR("Unknown tensor data type " + std::to_string((int)tensorDataType) + "."); return false; } void* cudaBuffer; size_t size = elementCount * sizeOfTensorData; auto err = cudaMalloc(&cudaBuffer, size); s_logger.log(err); if (err != cudaSuccess) { LOG_ERROR("Failed to allocate CUDA memory"); return false; } s_logger.log(cudaMemset(cudaBuffer, 0, size)); m_context->setTensorAddress(tensorName, cudaBuffer); if (tensorIOMode == nvinfer1::TensorIOMode::kINPUT) { m_inputBuffers[tensorName] = cudaBuffer; } else { m_outputBuffers[tensorName] = cudaBuffer; } } // Set input tensor shapes to specified batch size for (int i = 0; i < tensorCount; ++i) { const auto tensorName = m_engine->getIOTensorName(i); if (m_engine->getTensorIOMode(tensorName) != nvinfer1::TensorIOMode::kINPUT) continue; auto tensorShape = m_tensorShapes[tensorName]; m_context->setInputShape(tensorName, tensorShape); } return true; } void DestroyBuffers() { for (auto& kv : m_inputBuffers) { s_logger.log(cudaFree(kv.second)); } for (auto& kv : m_outputBuffers) { s_logger.log(cudaFree(kv.second)); } m_inputBuffers.clear(); m_outputBuffers.clear(); m_tensorShapes.clear(); } void Destroy() { DestroyBuffers(); m_context.reset(); m_engine.reset(); m_runtime.reset(); } bool ValidateTensorSize(const std::string& name, size_t byteCount) const { if (m_tensorShapes.empty()) { LOG_ERROR("ValidateTensorSize: InitInputs has not been called."); return false; } if (!m_tensorShapes.contains(name)) { LOG_ERROR("ValidateTensorSize: Tensor " + name + " not found."); return false; } size_t elementCount = 1; const auto& tensorShape = m_tensorShapes.at(name); for (int32_t d = 0; d < tensorShape.nbDims; ++d) elementCount *= tensorShape.d[d]; const size_t expectedByteCount = elementCount * m_dataTypeSizes.at(name); if (expectedByteCount != byteCount) { LOG_ERROR( "Invalid data size for tensor [" + name + "] containing " + std::to_string(elementCount) + " elements. Expected " + std::to_string(expectedByteCount) + " bytes, got " + std::to_string(byteCount) + " bytes instead."); return false; } return true; } std::vector GetInputBufferNames() const { std::vector names; names.reserve(m_inputBuffers.size()); for (const auto& pair : m_inputBuffers) { names.push_back(pair.first); } return names; } std::vector GetOutputBufferNames() const { std::vector names; names.reserve(m_outputBuffers.size()); for (const auto& pair : m_outputBuffers) { names.push_back(pair.first); } return names; } void SetInputData(const std::string& name, const void* data, size_t byteCount) { if(!ValidateTensorSize(name, byteCount)) { LOG_ERROR("SetInputData: Tensor " + name + " is not valid."); return; } s_logger.log(cudaMemcpy(m_inputBuffers[name], data, byteCount, cudaMemcpyHostToDevice)); } void SetInputDataAsync(const std::string& name, const void* data, size_t byteCount, cudaStream_t stream) { if(!ValidateTensorSize(name, byteCount)) { LOG_ERROR("SetInputDataAsync: Tensor " + name + " is not valid."); return; } s_logger.log(cudaMemcpyAsync(m_inputBuffers[name], data, byteCount, cudaMemcpyHostToDevice, stream)); } void GetOutputData(const std::string& name, void* data, size_t byteCount) { if(!ValidateTensorSize(name, byteCount)) { LOG_ERROR("GetOutputData: Tensor " + name + " is not valid."); return; } s_logger.log(cudaMemcpy(data, m_outputBuffers[name], byteCount, cudaMemcpyDeviceToHost)); } void GetOutputDataAsync(const std::string& name, void* data, size_t byteCount, cudaStream_t stream) { if(!ValidateTensorSize(name, byteCount)) { LOG_ERROR("GetOutputDataAsync: Tensor " + name + " is not valid."); return; } s_logger.log(cudaMemcpyAsync(data, m_outputBuffers[name], byteCount, cudaMemcpyDeviceToHost, stream)); } bool Enqueue( cudaStream_t stream ) { if (m_tensorShapes.empty()) { LOG_ERROR("Enqueue: InitInputs has not been called."); return false; } if (!m_context->allInputDimensionsSpecified()) { LOG_ERROR("Inference engine inputs aren't defined."); return false; } if(!m_context->enqueueV3(stream)) { LOG_ERROR("Enqueue: Failed to enqueue."); return false; } return true; } bool GetTensorShape(std::string name, std::vector& shape) const { if (m_tensorShapes.empty()) { LOG_ERROR("ValidateTensorSize: InitInputs has not been called."); return false; } if (!m_tensorShapes.contains(name)) { LOG_ERROR("ValidateTensorSize: Tensor " + name + " not found."); return false; } const auto& tensorShape = m_tensorShapes.at(name); shape.clear(); for (int dim=0; dim < tensorShape.nbDims; ++dim) { shape.push_back(tensorShape.d[dim]); } return true; } nvinfer1::DataType GetTensorDataType(std::string name) const { auto tensorDataType = m_engine->getTensorDataType(name.c_str()); return tensorDataType; } std::unique_ptr m_context = nullptr; std::unique_ptr m_engine = nullptr; std::unique_ptr m_runtime = nullptr; std::map m_tensorShapes; std::map m_inputBuffers; std::map m_outputBuffers; std::map m_dataTypeSizes; Options::AxisNames m_axis_names; }; TRTInferenceEngine::TRTInferenceEngine() : m_impl( new Impl ) { } TRTInferenceEngine::~TRTInferenceEngine() { } bool TRTInferenceEngine::Initialize(const std::string& trtPath, int deviceIndex, const Options::AxisNames& axisNames) { return m_impl->Initialize(trtPath, deviceIndex, axisNames); } bool TRTInferenceEngine::InitInputs(const AxisSizes& axisSizes) { return m_impl->InitInputs(axisSizes); } void TRTInferenceEngine::Destroy() { m_impl->Destroy(); } std::vector TRTInferenceEngine::GetInputTensorNames() const { return m_impl->GetInputBufferNames(); } std::vector TRTInferenceEngine::GetOutputTensorNames() const { return m_impl->GetOutputBufferNames(); } void TRTInferenceEngine::SetInputData(const std::string& name, const void* data, size_t byteCount) { m_impl->SetInputData(name, data, byteCount); } void TRTInferenceEngine::GetOutputData(const std::string& name, void* data, size_t byteCount) { m_impl->GetOutputData( name, data, byteCount); } void TRTInferenceEngine::SetInputDataAsync(const std::string& name, const void* data, size_t byteCount, cudaStream_t stream) { m_impl->SetInputDataAsync(name, data, byteCount, stream); } void TRTInferenceEngine::GetOutputDataAsync(const std::string& name, void* data, size_t byteCount, cudaStream_t stream) { m_impl->GetOutputDataAsync(name, data, byteCount, stream); } bool TRTInferenceEngine::Enqueue(cudaStream_t stream) { return m_impl->Enqueue(stream); } bool TRTInferenceEngine::GetTensorShape(std::string name, std::vector& shape) const { return m_impl->GetTensorShape(name, shape); } DataType TRTInferenceEngine::GetTensorDataType(std::string name) const { auto dataType = m_impl->GetTensorDataType(name); switch (dataType) { case nvinfer1::DataType::kFLOAT: return DataType::FLOAT; case nvinfer1::DataType::kHALF: return DataType::HALF; case nvinfer1::DataType::kINT8: return DataType::INT8; case nvinfer1::DataType::kINT32: return DataType::INT32; case nvinfer1::DataType::kBOOL: return DataType::BOOL; case nvinfer1::DataType::kUINT8: return DataType::UINT8; case nvinfer1::DataType::kINT64: return DataType::INT64; default: LOG_ERROR("ValidateTensorSize: Tensor " + name + " not found."); return DataType::UNKNOWN; } }