TensorRT 4 開發者手冊 中文版 (三-3)
本手冊為TensorRT 4.0.1.6 GA版英文手冊翻譯而來,博主英文水平一般般,主要作為備忘所用,分享出來以供更多開發者使用。TensorRT Developer Guide手冊一共分為四個章節,主要內容在第二、三章,看懂這兩章,寫程式碼夠用了。第一章為TensorRT綜述,就是自吹有多牛逼。第四章為示例,介紹demo的程式碼結構及功能。開篇是目錄,前三章每章為兩到三篇,最後第四章示例,會拆分幾個關鍵示例進行詳細說明。 第二章,分別介紹了C++和Python兩種語言介面下,TensorRT如何實現推理。下一篇文章再介紹Python介面如何實現推理。
[TOC] #第二章 TensorRT任務 本章主要內容是使用者能使用TensorRT實現的目標和任務。假設已有訓練好的模型,本章將覆蓋使用TensorRT的必要步驟: ‣匯入模型建立TensorRT網路定義
class Logger : public ILogger
{
void log(Severity severity, const char* msg) override
{
//不列印資訊性訊息
if (severity != Severity::kINFO)
std::cout << msg << std::endl;
}
} gLogger;
日誌記錄介面可用於建立多個執行時和構建器例項,但是日誌記錄介面是一個單件
(整個應用程式中只有一個類例項且這個例項所佔資源在整個應用程式中是共享的),所以你必須為每個物件使用同一個日誌記錄例項。
建立構建器或執行時時,將建立執行緒關聯的GPU上下文。 雖然預設上下文如果不存在,會自動建立它,但還是建議建立構建器或執行時例項之前,建立和配置CUDA上下文。
##2.2 C++建立網路定義
使用TensorRT進行推理的第一步是匯入你的模型並建立TensorRT網路。 實現此目的的最簡單方法是使用TensorRT解析器庫匯入模型,目前支援以下格式的序列化模型:
‣Caffe
感覺應該是大量呼叫
)API呼叫以定義網路圖中的每個層,併為模型的訓練引數實現自己的匯入機制。
在任何一種情況下,你都需要明確告訴TensorRT哪些張量是推理的輸出。 未標記為輸出的張量被認為是可由構建器優化的瞬態值。 輸出張量的數量沒有限制,但是將張量標記為輸出可能會禁止對張量進行一些優化。 輸入和輸出張量必須給出名稱(ITensor:: setName()
)。 在推理時,你需要為引擎提供一個指向輸入和輸出緩衝區的指標陣列。 為了確定引擎對應的緩衝區指標順序,你可以使用張量名稱進行查詢(ICudaEngine::getBindingIndex(const char* name)
)。
TensorRT網路定義的一個重要方面是它包含指向模型權重的指標,這些指標由構建器複製到優化引擎中。 如果網路是通過解析器建立的,則解析器將擁有權重記憶體的控制代碼,因此在構建器執行結束之前,不應刪除解析器物件。
###2.2.1 C++使用解析器匯入模型
使用C++解析器API匯入模型,需要執行以下主要操作步驟:
1. 建立構建器和網路
2. 針對指定格式,建立相應的解析器
3. 使用解析器解析匯入的模型並填充網路。
先建立構建器(作為網路工廠),再建立網路。 不同的解析器具有用於標記網路輸出的不同機制。
###2.2.2 使用C++解析器API匯入Caffe模型
下面的步驟說明了如何使用c++解析器API匯入Caffe模型。更多資訊,請參見示例sampleMNIST。
1. 建立構建器和網路
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
2. 建立Caffe解析器
ICaffeParser* parser = createCaffeParser();
3. 解析模型
const IBlobNameToTensor* blobNameToTensor = parser->parse("deploy_file" ,"modelFile", *network, DataType::kFLOAT);
這步用於將Caffe模型權值填充到TensorRT網路,最後一個引數指示解析器生成fp32權重的網路。 使用DataType :: kHALF將生成fp16權重的網路。除填充網路定義之外,解析器還將返回一個Blob字典,該字典從Caffe Blob名稱對映到TensorRT張量。 與Caffe不同,TensorRT網路定義沒有原位操作的概念。 當Caffe模型使用原位操作時,返回的TensorRT張量是字典中這個Blob的最後一次寫入。 例如,如果卷積寫入一個Blob且後面跟著ReLU,則這個Blob的名稱將對映到TensorRT張量,該張量就是ReLU的輸出。 4. 指定網路輸出
for (auto& s : outputs)
network->markOutput(*blobNameToTensor->find(s.c_str()));
###2.2.3 使用C++ UFF解析器API匯入TensorFlow模型 匯入TensorFlow框架,要求你將TensorFlow模型轉換為中間格式UFF(通用框架格式)。 有關轉換的更多資訊,請參閱將凍結圖(FrozenGraph)轉換為通用框架格式(UFF)。 1. 建立構建器和網路
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
2. 建立UFF解析器
IUFFParser* parser = createUffParser();
3. 宣告網路的輸入輸出
parser->registerInput("Input_0", DimsCHW(1, 28, 28), UffInputOrder::kNCHW);
parser->registerOutput("Binary_3");//如果用uff.from_tensorflow_frozen_model(frozen_file, output_nodes=None, preprocessor=None, **kwargs)轉換,輸出節點會預設為MarkOutput_0,MarkOutput_1......MarkOutput_N
注:TensorRT預設輸入張量是CHW,從TensorFlow(預設NHWC)匯入時,確保輸入張量也是CHW,如果不是先轉換為CHW。
4. 解析模型並填充網路定義
parser->parse(uffFile, *network, nvinfer1::DataType::kFLOAT);
###2.2.4 使用C++解析器API匯入ONNX模型 下面的步驟說明如何使用C ++ Parser API匯入ONNX模型。 有關ONNX匯入的更多資訊,請參考示例sampleOnnxMNIST。 1. 建立ONNX解析器。 解析器使用輔助配置管理檔案來將輸入引數傳遞給解析器物件:
nvonnxparser::IOnnxConfig* config = nvonnxparser::createONNXConfig();
//Create Parser
nvonnxparser::IONNXParser* parser = nvonnxparser::createONNXParser(*config);
2. 解析模型
parser->parse(onnx_filename, DataType::kFLOAT);
3. 轉換模型為TensorRT網路
parser->convertToTRTNetwork();
4. 從模型獲取TensorRT網路
nvinfer1::INetworkDefinition* trtNetwork = parser->getTRTNetwork();
##2.3 使用C++ API建立網路
你也可以通過TensorRT網路定義API直接定義網路,而不是使用解析器。 此方案假設在網路建立期間,儲存主機記憶體每層權重分別傳遞給TensorRT。
在下面的示例中,我們將建立一個包含Input
,Convolution
,Pooling
,FullyConnected
,Activation
和SoftMax
層的簡單網路。 有關更多資訊,請參考示例sampleMNISTAPI。
1. 建立構建器和網路
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
2. 新增輸入層,並傳入引數(輸入維度)。一個網路可以有多個輸入。
auto data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H,INPUT_W});
3. 添加捲積層,並傳入引數(input tensor、strides、weights)
layerName->getOutput(0)
auto conv1 = network->addConvolution(*data->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]);
conv1->setStride(DimsHW{1, 1});
4. 新增池化層
auto pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{2, 2});
pool1->setStride(DimsHW{2, 2});
5. 新增全連線層和啟用層
auto ip1 = network->addFullyConnected(*pool1->getOutput(0), 500,
weightMap["ip1filter"], weightMap["ip1bias"]);
auto relu1 = network->addActivation(*ip1->getOutput(0),
ActivationType::kRELU);
6. 新增softmax層計算最後的置信度,並將它設定為輸出
auto prob = network->addSoftMax(*relu1->getOutput(0));
prob->getOutput(0)->setName(OUTPUT_BLOB_NAME);
7. 標記輸出
network->markOutput(*prob->getOutput(0));
##2.4 C++構建推理引擎
下一步是呼叫TensorRT構建器來建立優化的執行時。 構建器的一個功能是搜尋CUDA kernel目錄以獲得最快的可用實現,因此必須使用相同的GPU來執行優化後的引擎。
構建器具有許多屬性,你可以設定這些屬性以控制網路執行的精度(一般有fp32、fp16、int8),以及自動調整引數,例如TensorRT在確定哪個實現最快時,需要迭代每個核心多少次(多次迭代會導致更長的執行時間,但是會降低噪聲的敏感性)。你還可以查詢構建器,以找出硬體本身支援的哪些精度型別。
兩個特別重要的屬性是最大批大小和最大工作空間大小。
‣最大批大小指定TensorRT將優化的批大小。 在執行時,可以選擇較小的批大小。
‣層演算法通常需要臨時工作空間。 此引數限制網路中任何層可以使用的最大空間大小。 如果提供的空間(scratch)不足,則TensorRT可能無法搜尋到給定層的優化實現。
1. 使用構建器例項構建引擎:
在構建引擎時,TensorRT會複製權重。
2. 如果需要使用,請先分配構建器、網路和解析器
##2.5 C++序列化模型
構建需要一些時間,因此一旦構建了引擎,你通常需要序列化引擎以供後後續使用。 在將模型用於推理之前,對模型進行序列化和反序列化不是必要的 - 如果需要,可以直接使用引擎例項進行推理。
注:序列化引擎不能跨GPU和TensorRT版本。
1. 先用構建器建立引擎,然後序列化
IHostMemory *serializedModel = engine->serialize();
// store model to disk
// <…>
serializedModel->destroy();
2. 反序列化建立執行時例項
IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize,nullptr);
最後一個引數nullptr是使用自定義圖層的應用程式的外掛層工廠。 有關更多資訊,請參考使用自定義圖層擴充套件TensorRT。 ##2.6 C++執行推理 一旦你有了引擎,就能執行推理了。 1. 建立空間儲存中間啟用值。由於引擎包含網路定義和訓練引數,因此需要額外的空間。這些都儲存在執行上下文中:
IExecutionContext * context = engine-> createExecutionContext();
引擎可以具有多個執行上下文(每個上下文非同步執行一路
),允許一組權重用於多個並行推理任務。例如,你可以使用一個引擎和一個上下文(每個CUDA流非同步執行一路
),上下文使用多個並行CUDA流來實現並行影象處理。每個上下文需要與引擎在相同的GPU上建立。
2. 使用輸入和輸出Blob名稱來獲取相應的輸入和輸出索引:
int inputIndex = engine.getBindingIndex(INPUT_BLOB_NAME);
int outputIndex = engine.getBindingIndex(OUTPUT_BLOB_NAME);
3. 使用這些索引,設定指向GPU上輸入和輸出緩衝區的緩衝區陣列:
void * buffers [2];
buffers [inputIndex] = inputbuffer;
buffers [outputIndex] = outputBuffer;
4. TensorRT執行通常是非同步的,因此使用CUDA流非同步執行核函式kernel:
context.enqueue(batchSize,buffers,stream,nullptr);
通常先從主機記憶體非同步拷貝資料到GPU視訊記憶體(前面提到的輸入緩衝區),之後enqueue函式將執行核函式。 enqueue()
的最後一個引數是一個可選的CUDA事件,當輸入緩衝區的資料已經消費並且可以安全地重用這片視訊記憶體時,它將被髮出訊號(非同步中斷系統中很有用,即生產者-消費者模型)。要確定核函式以及非同步拷貝何時完成,請使用標準CUDA同步機制(如事件
)或等待流
。
##2.7 C++記憶體管理
TensorRT提供兩種機制,允許應用程式對裝置記憶體進行更多控制。
預設情況下,在建立IExecutionContext
時,會分配儲存啟用資料的GPU裝置記憶體。 要避免此分配,可呼叫ICudaEngine::createExecutionContextWithoutDeviceMemory
。 然後應用程式負責呼叫IExecutionContext::setDeviceMemory()
來提供執行網路所需的記憶體。 ICudaEngine::getDeviceMemorySize()
返回記憶體塊的大小。
此外,應用程式可以通過實現IGpuAllocator介面提供在構建和執行時使用的自定義分配器。 實現介面後,呼叫
setGpuAllocator(&allocator);
然後在IBuilder或IRuntime介面上,所有的裝置記憶體申請釋放都由IGpuAllocator介面。(沒研究手動分配有什麼優勢,TensorRT瓶頸其實是在GPU計算資源不足,而不在視訊記憶體
)