TensorRT 的 C++ API 使用詳解

1. TensorRT 的 C++ API 使用示例

進行推理,需要先創建IExecutionContext對象,要創建這個對象,就需要先創建一個ICudaEngine的對象(engine)。

兩種創建engine的方式:

  1. 使用模型文件創建engine,並可把創建的engine序列化後存儲到硬盤以便後面直接使用;
  2. 使用之前已經序列化存儲的engine,這種方式比較高效些,因爲解析模型並生成engine還是挺慢的。

無論哪種方式,都需要創建一個全局的iLogger對象,並被用來作爲很多TensorRT API方法的參數使用。如下是一個logger創建示例:

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;

2. 用 C++ API 創建TensorRT網絡

2.1. 使用 C++ 的 parser API 導入模型

1. 創建TensorRT builder和network

IBuilder* builder = createInferBuilder(gLogger);
nvinfer1::INetworkDefinition* network = builder->createNetwork();

2.針對特定格式創建TensorRT parser

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

3. 使用parser解析導入的模型並填充network

parser->parse(args);

具體的args要看使用什麼格式的parser。

必須在網絡之前創建構建器,因爲它充當網絡的工廠。 不同的解析器在標記網絡輸出時有不同的機制。

2.2. 使用 C++ Parser API 導入 Caffe 模型

1. 創建builder和network

IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();

2. 創建caffe parser

ICaffeParser* parser = createCaffeParser();

3. 解析導入的模型

const IBlobNameToTensor* blobNameToTensor = parser->parse("deploy_file", 
              "modelFile", 
              *network, 
              DataType::kFLOAT);

這將把Caffe模型填充到TensorRT的network。最後一個參數指示解析器生成權重爲32位浮點數的網絡。使用DataType :: kHALF將生成具有16位權重的模型。

除了填充網絡定義之外,parser還返回一個字典,該字典是從 Caffe 的 blob names 到 TensorRT 的 tensors 的映射。與Caffe不同,TensorRT網絡定義沒有in-place的概念。當Caffe模型使用in-place操作時,字典中返回的相應的TensorRT tensors是對那個blob的最後一次寫入。例如,如果是一個卷積寫入到了blob並且後面跟的是ReLU,則該blob的名字映射到TensorRT tensors就是ReLU的輸出。

4. 給network分配輸出

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

2.3. 使用 C++ Parser API 導入 TensorFlow 模型

對於一個新的工程,推薦使用集成的TensorFlow-TensorRT作爲轉換TensorFlow network到TensorRT的方法來進行推理。具體可參考:Integrating TensorFlow With TensorRT

從TensorFlow框架導入,需要先將TensorFlow模型轉換爲中間格式:UFF(Universal Framework Format)。相關轉換可參考Coverting A Frozen Graph to UFF

更多關於UFF導入的信息可以參考:https://docs.nvidia.com/deeplearning/sdk/tensorrt-sample-support-guide/index.html#mnist_uff_sample

先來看下如何用C++ Parser API來導入TensorFlow模型。

1. 創建builder和network

IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();

2. 創建UFF parser

IUFFParser* parser = createUffParser();

3. 向UFF parser聲明network的輸入和輸出

parser->registerInput("Input_0", DimsCHW(1, 28, 28), UffInputOrder::kNCHW);
parser->registerOutput("Binary_3");

注意:TensorRT期望的輸入tensor是CHW順序的。從TensorFlow導入時務必確保這一點,如不是CHW,那就先轉換成CHW順序的。

4. 解析已導入的模型到network

parser->parse(uffFile, *network, nvinfer1::DataType::kFLOAT);

2.4. 使用 C++ Parser API 導入 ONNX 模型

使用限制:注意版本問題,TensorRT5.1 附帶的 ONNX Parser支持的ONNX IR版本是0.0.3,opset版本是9。通常,較新的ONNX parser是後向兼容的。更多信息可參考:ONNX Model Opset Version Converteronnx-tensorrt

更多關於ONNX導入的信息也可參考:
https://docs.nvidia.com/deeplearning/sdk/tensorrt-sample-support-guide/index.html#onnx_mnist_sample

1. 創建ONNX parser,parser使用輔助配置管理SampleConfig對象將輸入參數從示例的可執行文件傳遞到parser對象

nvonnxparser::IOnnxConfig* config = nvonnxparser::createONNXConfig();
//Create Parser
nvonnxparser::IONNXParser* parser = nvonnxparser::createONNXParser(*config);

2. 填充模型

parser->parse(onnx_filename, DataType::kFLOAT);

3. 轉換模型到TensorRT的network

parser->convertToTRTNetwork();

4. 從模型獲取network

nvinfer1::INetworkDefinition* trtNetwork = parser->getTRTNetwork();

3. 用 C++ API 構建 engine

下一步是調用TensorRT的builder來創建優化的runtime。 builder的其中一個功能是搜索其CUDA內核目錄以獲得最快的實現,因此用來構建優化的engine的GPU設備和實際跑的GPU設備一定要是相同的纔行。

builder具有許多屬性,可以通過設置這些屬性來控制網絡運行的精度,以及自動調整參數。還可以查詢builder以找出硬件本身支持的降低的精度類型。

有兩個特別重要的屬性:最大batch size和最大workspace size。

  • 最大batch size指定TensorRT將要優化的batch大小。在運行時,只能選擇比這個值小的batch。
  • 各種layer算法通常需要臨時工作空間。這個參數限制了網絡中所有的層可以使用的最大的workspace空間大小。 如果分配的空間不足,TensorRT可能無法找到給定層的實現。

1. 用builder對象創建構建engine

builder->setMaxBatchSize(maxBatchSize);
builder->setMaxWorkspaceSize(1 << 20);
ICudaEngine* engine = builder->buildCudaEngine(*network);

2. 用完分配過的network,builder和parser記得解析

parser->destroy();
network->destroy();
builder->destroy();

4. 用 C++ API 序列化一個模型

序列化模型,即把engine轉換爲可存儲的格式以備後用。推理時,再簡單的反序列化一下這個engine即可直接用來做推理。通常創建一個engine還是比較花時間的,可以使用這種序列化的方法避免每次重新創建engine。

注意:序列化的engine不能跨平臺或在不同版本的TensorRT間移植使用。因爲其生成是根據特定版本的TensorRT和GPU的。

1. 序列化

IHostMemory *serializedModel = engine->serialize();
// store model to disk
// <…>
serializedModel->destroy();

2. 創建一個runtime並用來反序列化

IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize, nullptr);

5. 用 C++ API 執行推理

1. 創建一個Context用來存儲中間激活值

IExecutionContext *context = engine->createExecutionContext();

一個engine可以有多個execution context,並允許將同一套weights用於多個推理任務。可以在並行的CUDA streams流中按每個stream流一個engine和一個context來處理圖像。每個context在engine相同的GPU上創建。

2. 用input和output的blob名字獲取對應的input和output的index

int inputIndex = engine.getBindingIndex(INPUT_BLOB_NAME);
int outputIndex = engine.getBindingIndex(OUTPUT_BLOB_NAME);

3. 使用上面的indices,在GPU上創建一個指向input和output緩衝區的buffer數組

void* buffers[2];
buffers[inputIndex] = inputbuffer;
buffers[outputIndex] = outputBuffer;

4. 通常TensorRT的執行是異步的,因此將kernels加入隊列放在CUDA stream流上

context.enqueue(batchSize, buffers, stream, nullptr);

通常在kernels之前和之後來enquque異步memcpy()以從GPU移動數據(如果尚未存在)。

enqueue()的最後一個參數是一個可選的CUDA事件,當輸入緩衝區被消耗且它們的內存可以安全地重用時這個事件便會被信號觸發。

爲了確定kernels(或可能存在的memcpy())何時完成,請使用標準CUDA同步機制(如事件)或等待流。

6. C++ API 的內存管理

TensorRT提供了兩種機制來允許應用程序對設備內存進行更多的控制。

默認情況下,在創建IExecutionContext時,會分配持久設備內存來保存激活數據。爲避免這個分配,請調用createExecutionContextWithoutDeviceMemory。然後應用程序會調用IExecutionContext :: setDeviceMemory()來提供運行網絡所需的內存。內存塊的大小由ICudaEngine :: getDeviceMemorySize()返回。

此外,應用程序可以通過實現IGpuAllocator接口來提供在構建和運行時使用的自定義分配器。實現接口後,請調用:

setGpuAllocator(&allocator);

IBuilderIRuntime接口上。所有的設備內存都將通過這個接口來分配和釋放。

7. 調整engine

TensorRT可以爲一個engine裝填新的weights,而不用重新build。

1. 在build之前申請一個可refittable的engine

...
builder->setRefittable(true); 
builder->buildCudaEngine(network);

2. 創建一個refitter對象

ICudaEngine* engine = ...;
IRefitter* refitter = createInferRefitter(*engine,gLogger)

3. 更新你想更新的weights
如:爲一個叫MyLayer的卷積層更新kernel weights

Weights newWeights = ...;
refitter.setWeights("MyLayer",WeightsRole::kKERNEL,
                    newWeights);

這個新的weights要和原始用來build engine的weights具有相同的數量。
setWeights出錯時會返回false。

4. 找出哪些weights需要提供。
這通常需要調用兩次IRefitter::getMissing,第一次調用得到Weights對象的數目,第二次得到他們的layers和roles。

const int n = refitter->getMissing(0, nullptr, nullptr);
std::vector<const char*> layerNames(n);
std::vector<WeightsRole> weightsRoles(n);
refitter->getMissing(n, layerNames.data(), 
                        weightsRoles.data());

5. 提供missing的weights(順序無所謂)

for (int i = 0; i < n; ++i)
    refitter->setWeights(layerNames[i], weightsRoles[i],
                         Weights{...});

只需提供missing的weights即可,如果提供了額外的weights可能會觸發更多weights的需要。

6. 更新engine

bool success = refitter->refitCudaEngine();
assert(success);

如果success的值爲false,可以檢查一下diagnostic log,也許有些weights還是missing的。

7. 銷燬refitter

refitter->destroy();

如果想要查看engine中所有可重新調整的權重,可以使用refitter-> getAll(...),類似於步驟4中的如何使用getMissing。


參考

DEEP LEARNING SDK DOCUMENTATION

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章