2. Working With TensorRT Using The C++ API

以下部分重點介紹了可以使用C ++ API執(zhí)行的TensorRT用戶目標和任務。 進一步的細節(jié)在 Samples 部分提供,并在適當?shù)那闆r下鏈接到下面。

假設您從一個已經(jīng)訓練好的模型開始。 本章將介紹使用TensorRT的以下必要步驟:

  • 從模型中創(chuàng)建 TensorRT 網(wǎng)絡定義
  • 調(diào)用 TensorRT 構(gòu)建器以從網(wǎng)絡創(chuàng)建優(yōu)化的運行時引擎
  • 序列化和反序列化引擎,以便在運行時快速重新創(chuàng)建
  • 喂入數(shù)據(jù)為引擎提供執(zhí)行推理

C++ API vs Python API

從本質(zhì)上講,C ++ API 和 Python API 在支持您的需求方面應該完全相同。 C ++ API 應該用于任何性能關鍵場景,以及安全性很重要的場合,例如汽車行業(yè)。

Python API 的主要好處是數(shù)據(jù)預處理和后處理易于使用,因為您可以使用各種庫,如 NumPy 和 SciPy。 有關 Python API 的更多信息,請參閱 Working With TensorRT Using The Python API.

2.1. Instantiating TensorRT Objects in C++

要運行推理,您需要使用 IExecutionContext 對象。 要創(chuàng)建 IExecutionContext 類型的對象,首先需要創(chuàng)建 ICudaEngine 類型的對象(引擎)。

可以通過以下兩種方式之一創(chuàng)建引擎:

  • 通過用戶模型的網(wǎng)絡定義。 在這種情況下,可以選擇將引擎序列化并保存以供以后使用。
  • 通過從磁盤讀取序列化引擎。 在這種情況下,性能更好,因為繞過了解析模型和創(chuàng)建中間對象的步驟。

需要全局創(chuàng)建 iLogger 類型的對象。 它用作 TensorRT API 的各種方法的參數(shù)。 一個演示記錄器創(chuàng)建的簡單示例如下所示:

class Logger : public ILogger           
 {
     void log(Severity severity, const char* msg) override
     {
         // suppress info-level messages
         if (severity != Severity::kINFO)
             std::cout << msg << std::endl;
     }
 } gLogger;

名為 createInferBuilder(gLogger) 的全局 TensorRT API 方法用于創(chuàng)建 iBuilder 類型的對象,如下圖所示。有關更多信息,請參閱 IBuilder class reference。

使用iLogger作為輸入?yún)?shù)創(chuàng)建iBuilder

為 iBuilder 定義的名為 createNetwork 的方法用于創(chuàng)建 iNetworkDefinition 類型的對象,如下圖所示。

createNetwork() 用于創(chuàng)建網(wǎng)絡

使用 iNetwork 定義作為輸入創(chuàng)建一個可用的解析器:

  • ONNX: parser = nvonnxparser::createParser(*network, gLogger);
  • NVCaffe: ICaffeParser* parser = createCaffeParser();
  • UFF: parser = createUffParser();

調(diào)用來自 iParser 類型的對象的名為 parse() 的方法來讀取模型文件并填充 TensorRT 網(wǎng)絡:

Parsing the model file

調(diào)用 iBuilder 的一個名為 buildCudaEngine() 的方法來創(chuàng)建一個 iCudaEngine 類型的對象,如圖所示:

Creating the TensorRT engine

可以選擇將引擎序列化并轉(zhuǎn)儲到文件中。

Creating the TensorRT engine

執(zhí)行上下文用于執(zhí)行推理。

Creating an execution context

如果序列化引擎被保留并保存到文件中,則可以繞過上述大多數(shù)步驟。

名為 createInferRuntime(gLogger)的全局 TensorRT API 方法用于創(chuàng)建 iRuntime 類型的對象,如圖所示:

Creating TensorRT runtime

有關 TensorRT 運行時的更多信息,請參閱 IRuntime class reference。 通過調(diào)用運行時方法 deserializeCudaEngine() 來創(chuàng)建引擎。

對于這兩種使用模型,其余推斷是相同的。

盡管可以避免創(chuàng)建 CUDA 上下文(將為您創(chuàng)建默認上下文),但這是不可取的。 建議在創(chuàng)建運行時或構(gòu)建器對象之前創(chuàng)建和配置 CUDA 上下文。

將使用與創(chuàng)建線程關聯(lián)的 GPU 上下文創(chuàng)建構(gòu)建器或運行時。 雖然如果默認上下文尚不存在,但會創(chuàng)建它,但建議在創(chuàng)建運行時或構(gòu)建器對象之前創(chuàng)建和配置 CUDA 上下文。

2.2. Creating A Network Definition In C++

使用 TensorRT 進行推理的第一步是從您的模型創(chuàng)建 TensorRT 網(wǎng)絡。 實現(xiàn)此目的的最簡單方法是使用 TensorRT 解析器庫導入模型,該解析器庫支持以下格式的序列化模型:

另一種方法是使用 TensorRT API 直接定義模型。 這要求您進行少量 API 調(diào)用以定義網(wǎng)絡圖中的每個層,并為模型的訓練參數(shù)實現(xiàn)自己的導入機制。

在任何一種情況下,您都明確需要告訴 TensorRT 需要哪些張量作為推斷的輸出。 未標記為輸出的張量被認為是可由建造者優(yōu)化的瞬態(tài)值。 輸出張量的數(shù)量沒有限制,但是,將張量標記為輸出可能會禁止對張量進行一些優(yōu)化。 輸入和輸出張量也必須給出名稱(使用 ITensor :: setName() )。 在推理時,您將為引擎提供一個指向輸入和輸出緩沖區(qū)的指針數(shù)組。 為了確定引擎對這些指針的預期順序,您可以使用張量名稱進行查詢。

TensorRT 網(wǎng)絡定義的一個重要方面是它包含指向模型權重的指針,這些指針由構(gòu)建器復制到優(yōu)化引擎中。 如果網(wǎng)絡是通過解析器創(chuàng)建的,則解析器將擁有權重占用的內(nèi)存,因此在構(gòu)建器運行之前,不應刪除解析器對象。

2.2.1. Creating A Network Definition From Scratch Using The C++ API

您也可以通過網(wǎng)絡定義 API 直接將網(wǎng)絡定義到 TensorRT,而不是使用解析器。 此方案假定在網(wǎng)絡創(chuàng)建期間,每層權重已準備好在主機內(nèi)存中傳遞給 TensorRT。

在下面的示例中,我們將創(chuàng)建一個包含 Input,Convolution,Pooling,F(xiàn)ullyConnected,Activation 和 SoftMax 層的簡單網(wǎng)絡。 要查看整體中的代碼,請參閱位于 /usr/src/tensorrt/samples/sampleMNISTAPI 目錄中的 sampleMNISTAPI

  1. 創(chuàng)建構(gòu)建器和網(wǎng)絡:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
  1. 使用輸入維度的方式將輸入層添加到網(wǎng)絡。 網(wǎng)絡可以有多個輸入,但在此示例中只有一個:
auto data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H, INPUT_W});
  1. 添加具有隱藏層輸入節(jié)點的 Convolution 圖層,該層還帶有過濾器和偏差的步幅和權重。 為了從圖層中檢索張量參考,我們可以使用:
layerName->getOutput(0)
auto conv1 = network->addConvolution(*data->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]);
conv1->setStride(DimsHW{1, 1});

注意:傳遞給 TensorRT 層的權重在主機內(nèi)存中。

  1. 添加 Pooling 層:
auto pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{2, 2});
pool1->setStride(DimsHW{2, 2});
  1. 添加全連接和激活函數(shù)層:
auto ip1 = network->addFullyConnected(*pool1->getOutput(0), 500, weightMap["ip1filter"], weightMap["ip1bias"]);
auto relu1 = network->addActivation(*ip1->getOutput(0), ActivationType::kRELU);
  1. 添加 SoftMax 層以計算最終概率并將其設置為輸出:
auto prob = network->addSoftMax(*relu1->getOutput(0));
prob->getOutput(0)->setName(OUTPUT_BLOB_NAME);
  1. 標記輸出:
network->markOutput(*prob->getOutput(0));

2.2.2. Importing A Model Using A Parser In C++

要使用 C ++ Parser API 導入模型,您需要執(zhí)行以下高級步驟:

  1. Create the TensorRT builder and network.
IBuilder* builder = createInferBuilder(gLogger);
nvinfer1::INetworkDefinition* network = builder->createNetwork();
  1. Create the TensorRT parser for the specific format.

ONNX

auto parser = nvonnxparser::createParser(*network, gLogger);

UFF

auto parser = createUffParser();

NVCaffe

ICaffeParser* parser = createCaffeParser();
  1. Use the parser to parse the imported model and populate the network.
parser->parse(args);

具體的 args 取決于使用什么格式的解析器。 有關更多信息,請參閱 TensorRT API 中解析器的文檔。

必須在網(wǎng)絡之前創(chuàng)建構(gòu)建器,因為它充當網(wǎng)絡的工廠。 不同的解析器具有用于標記網(wǎng)絡輸出的不同機制。

2.2.3. Importing A Caffe Model Using The C++ Parser API

以下步驟說明了如何使用 C ++ Parser API 導入 Caffe 模型。 有關更多信息,請參閱 sampleMNIST。

  1. Create the builder and network:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
  1. Create the Caffe parser:
ICaffeParser* parser = createCaffeParser();
  1. Parse the imported model:
const IBlobNameToTensor* blobNameToTensor = parser->parse("deploy_file" , "modelFile", *network, DataType::kFLOAT);

這將填充 Caffe 模型中的 TensorRT 網(wǎng)絡。 最后一個參數(shù)指示解析器生成權重為 32 位浮點數(shù)的網(wǎng)絡。 使用 DataType::kHALF 將生成具有 16 位權重的模型。

除了填充網(wǎng)絡定義之外,解析器還返回一個字典,該字典從 Caffe blob 名稱映射到 TensorRT 張量。 與 Caffe 不同,TensorRT 網(wǎng)絡定義沒有就地操作的概念。 當 Caffe 模型使用就地操作時,字典中返回的 TensorRT 張量對應于對該blob 的最后一次寫入。 例如,如果卷積寫入 blob 并且后跟就地 ReLU,則該 blob 的名稱將映射到 TensorRT 張量,該張量是 ReLU 的輸出。

  1. Specify the outputs of the network:
for (auto& s : outputs)
    network->markOutput(*blobNameToTensor->find(s.c_str()));

2.2.4. Importing A TensorFlow Model Using The C++ UFF Parser API

注意:對于新項目,建議使用 TensorFlow-TensorRT 集成 作為轉(zhuǎn)換 TensorFlow 網(wǎng)絡以使用 TensorRT 進行推理的方法。 有關集成說明,請參閱 Integrating TensorFlow With TensorRTRelease Notes

從 TensorFlow 框架導入要求您將 TensorFlow 模型轉(zhuǎn)換為中間格式 UFF(Universal Framework Format)。 有關轉(zhuǎn)換的更多信息,請參閱 Converting A Frozen Graph To UFF。

以下步驟說明了如何使用C ++ Parser API導入TensorFlow模型。 有關UFF導入的更多信息,請參閱 sampleUffMNIST。

  1. Create the builder and network:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
  1. Create the UFF parser:
IUFFParser* parser = createUffParser();
  1. Declare the network inputs and outputs to the UFF parser:
parser->registerInput("Input_0", DimsCHW(1, 28, 28), UffInputOrder::kNCHW);
parser->registerOutput("Binary_3");

注意:TensorRT 期望輸入張量為 CHW 順序。 從 TensorFlow 導入時,請確保輸入張量符合所需順序,如果不是,請將其轉(zhuǎn)換為 CHW。

  1. Parse the imported model to populate the network:
parser->parse(uffFile, *network, nvinfer1::DataType::kFLOAT);

2.2.5. Importing An ONNX Model Using The C++ Parser API

限制:由于 ONNX 格式正在快速開發(fā),您可能會遇到模型版本和解析器版本之間的版本不匹配。 TensorRT 5.0.0 附帶的 ONNX Parser 支持 ONNX IR(Intermediate Representation)版本 0.0.3,opset 版本 7。

通常,較新版本的 ONNX Parser 旨在向后兼容,因此,遇到早期版本的 ONNX 導出器生成的模型文件不應該導致問題。 當更改不向后兼容時,可能會有一些例外。 在這種情況下,將早期的 ONNX 模型文件轉(zhuǎn)換為以后支持的版本。 有關此主題的更多信息,請參閱 ONNX Model Opset Version Converter。

用戶模型也可能是由支持后來的 opset 的導出工具生成的,而不是 TensorRT 附帶的 ONNX 解析器所支持的。在這種情況下,請檢查發(fā)布到 GitHub onnx-tensorrt 的最新版本的 TensorRT 是否支持所需的版本。 支持的版本由 onnx_trt_backend.cpp 中的 BACKEND_OPSET_VERSION 變量定義。 從 GitHub 下載并構(gòu)建最新版本的 ONNX TensorRT Parser。 有關構(gòu)建的說明,請訪問:TensorRT backend for ONNX

以下步驟說明了如何使用 C ++ Parser API 導入 ONNX 模型。 有關 ONNX 導入的詳細信息,請參閱 sampleOnnxMNIST。

  1. Create the ONNX parser. 解析器使用輔助配置管理 SampleConfig 對象將輸入?yún)?shù)從示例可執(zhí)行文件傳遞到解析器對象:
nvonnxparser::IOnnxConfig* config = nvonnxparser::createONNXConfig();
//Create Parser
nvonnxparser::IONNXParser* parser = nvonnxparser::createONNXParser(*config);
  1. Ingest the model:
parser->parse(onnx_filename, DataType::kFLOAT);
  1. Convert the model to a TensorRT network:
parser->convertToTRTNetwork();
  1. Obtain the network from the model:
nvinfer1::INetworkDefinition* trtNetwork = parser->getTRTNetwork();

2.3. Building An Engine In C++

下一步是調(diào)用 TensorRT 構(gòu)建器來創(chuàng)建優(yōu)化的運行時。 構(gòu)建器的一個功能是搜索其 CUDA 內(nèi)核目錄以獲得最快的可用實現(xiàn),因此必須使用相同的 GPU 來構(gòu)建優(yōu)化引擎將運行的 GPU。

構(gòu)建器具有許多屬性,您可以設置這些屬性以控制網(wǎng)絡應運行的精度,以及自動調(diào)整參數(shù),例如 TensorRT 在確定哪個最快時(多次迭代會導致更長的運行時間,但是對噪聲的敏感性較低)應該為每個內(nèi)核計時多少次 。您還可以查詢構(gòu)建器,以找出硬件本身支持的精簡類型。

兩個特別重要的屬性是最大批量大小和最大工作空間大小。

  • 最大批量大小指定 TensorRT 將優(yōu)化的批量大小。 在運行時,可以選擇較小的批量大小。
  • 層算法通常需要臨時工作空間。 此參數(shù)限制網(wǎng)絡中任何層可以使用的最大大小。 如果提供的劃痕不足,則TensorRT可能無法找到給定層的實現(xiàn)。
  1. Build the engine using the builder object:
builder->setMaxBatchSize(maxBatchSize);
builder->setMaxWorkspaceSize(1 << 20);
ICudaEngine* engine = builder->buildCudaEngine(*network);

在構(gòu)建引擎時,TensorRT會復制權重。

  1. Dispense(分發(fā)、分配) with the network, builder, and parser if using one.
engine->destroy();
network->destroy();
builder->destroy();

2.4. Serializing A Model In C++

要進行序列化,您要將引擎轉(zhuǎn)換為一種格式,以便以后存儲和使用以進行推理。 要用于推理,您只需反序列化引擎即可。 序列化和反序列化是可選的。 由于從網(wǎng)絡定義創(chuàng)建引擎可能非常耗時,因此每次應用程序重新生成時都可以通過序列化一次并在推理時對其進行反序列化來避免重建引擎。 因此,在構(gòu)建引擎之后,用戶通常希望將其序列化以供以后使用。

構(gòu)建可能需要一些時間,因此一旦構(gòu)建了引擎,您通常需要將其序列化以供以后使用。 在將模型用于推理之前,并非絕對有必要對模型進行序列化和反序列化 - 如果需要,可以直接使用引擎對象進行推理。

注意:序列化引擎不能跨平臺或TensorRT版本移植。 引擎特定于它們構(gòu)建的精確GPU模型(除了平臺和TensorRT版本)。

  1. Run the builder as a prior offline step and then serialize:
IHostMemory *serializedModel = engine->serialize();
// store model to disk
// <…>
serializedModel->destroy();
  1. Create a runtime object to deserialize:
IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize, nullptr);

最后一個參數(shù)是使用自定義圖層的應用程序的插件層工廠。 有關更多信息,請參閱 Extending TensorRT With Custom Layers。

2.5. Performing Inference In C++

以下步驟說明了如何使用引擎在C ++中執(zhí)行推理。

  1. 創(chuàng)建一些空間來存儲中間激活值。 由于引擎保持網(wǎng)絡定義和訓練的參數(shù),因此需要額外的空間。 這些都保存在執(zhí)行上下文中:
IExecutionContext *context = engine->createExecutionContext();

引擎可以具有多個執(zhí)行上下文,允許一組權重用于多個重疊推理任務。 例如,您可以使用一個引擎和每個流一個上下文在并行 CUDA 流中處理圖像。 每個上下文將在與引擎相同的 GPU 上創(chuàng)建。

  1. Use the input and output blob names to get the corresponding input and output index:
int inputIndex = engine.getBindingIndex(INPUT_BLOB_NAME);
int outputIndex = engine.getBindingIndex(OUTPUT_BLOB_NAME);
  1. Using these indices, set up a buffer array pointing to the input and output buffers on the GPU:
void* buffers[2];
buffers[inputIndex] = inputbuffer;
buffers[outputIndex] = outputBuffer;
  1. TensorRT execution is typically asynchronous, so enqueue the kernels on a CUDA stream:
context.enqueue(batchSize, buffers, stream, nullptr);

通常在內(nèi)核之前和之后將異步 memcpy() 排入隊列以從 GPU 移動數(shù)據(jù)(如果尚未存在)。 enqueue() 的最后一個參數(shù)是一個可選的 CUDA 事件,當輸入緩沖區(qū)被占用并且可以安全地重用它們的內(nèi)存時,它將被發(fā)出信號。

要確定內(nèi)核(以及可能的 memcpy() )何時完成,請使用標準 CUDA 同步機制(如事件)或等待流。

2.6. Memory Management In C++

TensorRT 提供了兩種機制,允許應用程序更多地控制設備內(nèi)存。

默認情況下,在創(chuàng)建 IExecutionContext 時,會分配持久設備內(nèi)存來保存激活數(shù)據(jù)。 要避免此分配,請調(diào)用 createExecutionContextWithoutDeviceMemory。 然后應用程序負責調(diào)用IExecutionContext :: setDeviceMemory() 來提供運行網(wǎng)絡所需的內(nèi)存。 ICudaEngine :: getDeviceMemorySize() 返回內(nèi)存塊的大小。

此外,應用程序可以通過實現(xiàn) IGpuAllocator 接口提供在構(gòu)建和運行時使用的自定義分配器。 實現(xiàn)接口后,請調(diào)用 setGpuAllocator(分配器);

IBuilderIRuntime 接口上。 然后將通過此接口分配和釋放所有設備內(nèi)存。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容