1. 程式人生 > 其它 >TensorRT筆記(5)使用自定義層擴充套件TensorRT

TensorRT筆記(5)使用自定義層擴充套件TensorRT

技術標籤:TensorRT

4.使用自定義層擴充套件TensorRT

4.使用自定義層擴充套件TensorRT

NVIDIA®TensorRT™支援多種型別的圖層,並且其功能不斷擴充套件。 但是,在某些情況下,受支援的圖層無法滿足模型的特定需求。

在這種情況下,使用者可以使用針對C ++和Python API的IPluginV2Ext類實現自定義層,從而擴充套件TensorRT功能。 自定義層(通常稱為外掛)由應用程式實現和例項化,並且它們的生存期必須跨越它們在TensorRT引擎中的使用。

TensorRT層(不包括TopK)可以在零工作空間的情況下工作,但是,如果沒有使用零工作空間的實現,則可以忽略所要求的精度。 在後一種情況下,即使將精度設定為其他值,該層也將在FP32上執行。

4.1 使用C ++ API新增自定義層

通過擴充套件類IPluginCreator和TensorRT的外掛基類之一來實現自定義層。
IPluginCreator

是自定義圖層的建立者類,使用者可以使用該類來獲取外掛名稱,版本和外掛欄位引數。 它還提供了在網路構建階段建立外掛物件並在推理過程中反序列化它的方法。

必須從外掛的基類之一派生您的外掛類。 它們在支援具有不同型別/格式的輸入/輸出或具有動態形狀的網路方面具有變化的表達能力。 下表總結了基類,從最低表現到最高表現排序。
Table 1. Base classes, ordered from least expressive to most expressive

IPluginV2Ext
IPluginV2IOExt
IPluginV2DynamicExt
所有這些基本類都包括版本控制支援,並幫助啟用支援NCHW和單精度之外的其他資料格式的自定義層。
注意:如果使用IPluginV2Ext,IPluginV2IOExt或IPluginV2DynamicExt,則應始終提供外掛的FP32實現,以允許外掛在任何網路上正常執行。
注意:在6.0.1之前的TensorRT版本中,您是從IPluginV2或IPluginV2Ext派生自定義層的。儘管仍支援這些API,但我們強烈建議您遷移到IPluginV2IOExtIPluginV2DynamicExt,以便能夠使用所有新的外掛功能。
TensorRT還提供了通過呼叫REGISTER_TENSORRT_PLUGIN(pluginCreator)來註冊外掛的功能,該外掛將外掛建立器靜態註冊到外掛登錄檔。在執行時,可以使用外部函式getPluginRegistry()查詢外掛登錄檔。外掛登錄檔儲存指向所有已註冊外掛建立者的指標,可用於根據外掛名稱和版本查詢特定的外掛建立者。 TensorRT庫包含可以載入到您的應用程式中的外掛。有關我們的開源外掛的列表,請參見GitHub:TensorRT外掛
注意:要在您的應用程式中使用TensorRT註冊的外掛,必須載入libnvinfer_plugin.so庫,並且必須註冊所有外掛。這可以通過在應用程式程式碼中呼叫initLibNvInferPlugins(void* logger,const char * libNamespace)()來完成。
注意:如果您有自己的外掛庫,則可以包括一個相似的入口點,以在唯一的名稱空間下在登錄檔中註冊所有外掛。這樣可以確保在構建期間不同外掛庫之間不會發生外掛名稱衝突。
有關這些外掛的更多資訊,請參見NvInferPlugin.h檔案以供參考。

使用外掛建立器,可以呼叫IPluginCreator :: createPlugin()函式,該函式返回IPluginV2型別的外掛物件。可以使用addPluginV2()將物件新增到TensorRT網路中,後者會建立一個圖層並將其新增到網路,然後將該圖層繫結到給定的外掛。該方法還返回指向該層(型別為IPluginV2Layer)的指標,該指標可用於訪問該層或外掛本身(通過getPlugin())。

例如,要將外掛名稱設定為pluginName且版本設定為pluginVersion的外掛層新增到網路中,可以發出以下命令:

//Use the extern function getPluginRegistry to access the global TensorRT Plugin Registry
auto creator = getPluginRegistry()->getPluginCreator(pluginName, pluginVersion);
const PluginFieldCollection* pluginFC = creator->getFieldNames();
//populate the field parameters (say layerFields) for the plugin layer 
PluginFieldCollection *pluginData = parseAndFillFields(pluginFC, layerFields); 
//create the plugin object using the layerName and the plugin meta data
IPluginV2 *pluginObj = creator->createPlugin(layerName, pluginData);
//add the plugin to the TensorRT network using the network API
auto layer = network.addPluginV2(&inputs[0], int(inputs.size()), pluginObj);(build rest of the network and serialize engine)
pluginObj->destroy() // Destroy the plugin object(destroy network, engine, builder)(free allocated pluginData)

注意:pluginData應該在傳遞給createPlugin之前在堆上分配PluginField條目。
注意:上面的createPlugin方法將在堆上建立一個新的外掛物件,並返回指向它的指標。確保如上所述銷燬pluginObj,以避免記憶體洩漏。
序列化期間,TensorRT引擎將在內部儲存所有IPluginV2型別外掛的外掛型別,外掛版本和名稱空間(如果存在)。在反序列化期間,TensorRT引擎會查詢此資訊,以從外掛登錄檔中找到外掛建立者。這使TensorRT引擎可以在內部呼叫IPluginCreator :: deserializePlugin()方法。 TensorRT引擎將通過呼叫IPluginV2 :: destroy()方法在內部反序列化期間建立的外掛物件銷燬。

在TensorRT的先前版本中,您必須實現nvinfer1 :: IPluginFactory類以在反序列化期間呼叫createPlugin方法。使用TensorRT註冊並使用addPluginV2新增的外掛不再需要此功能。

4.1.1 示例:使用C ++新增自定義層

要在C ++中新增自定義層,請從使用C ++ API新增自定義層中描述的基類之一派生它。 因為此示例不需要動態形狀,所以它使用IPluginV2IOExt

對於基於Caffe的網路,如果使用TensorRT Caffe解析器,您還將從nvcaffeparser1 :: IPluginFactoryExt(對於IPluginExt型別的外掛)和nvinfer1 :: IPluginFactory派生類。 有關更多資訊,請參見從框架匯入模型時使用自定義層

以下示例程式碼添加了一個名為FooPlugin的新外掛:

class FooPlugin : public IPluginV2IOExt
{
	...override all pure virtual methods of IPluginV2IOExt with definitions for your plugin.  Do not override the TRT_DEPRECATED methods.
};

class MyPluginFactory : public nvinfer1::IPluginFactory
{
	...implement all factory methods for your plugin
};

如果您正在使用向IPluginV2型別的TensorRT外掛登錄檔註冊的外掛,則無需實現類nvinfer1 :: IPluginFactory。

4.1.2 示例:使用C ++新增Caffe不支援的自定義層

如果TensorRT Caffe解析器必須處理您的外掛,則步驟與示例1類似,但是您需要改為實現nvcaffeparser1 :: IPluginFactoryV2IPluginCreator類並註冊它們。

class FooPlugin : public IPluginV2IOExt
{
	...implement all class methods for your plugin
};

class FooPluginFactory : public nvcaffeparser1::IPluginFactoryV2
{
	virtual nvinfer1::IPluginV2* createPlugin(...)
	{
		...create and return plugin object of type FooPlugin
	}
	bool isPlugin(const char* name) 
	{
		...check if layer name corresponds to plugin
	}
}

class FooPluginCreator : public IPluginCreator
{
	...implement all creator methods here
};
REGISTER_TENSORRT_PLUGIN(FooPluginCreator);

以下示例說明了如何使用C ++為Caffe網路新增自定義外掛層:

4.1.3 示例:使用C ++新增UFF不支援的自定義層

為了使用TensorRT執行TensorFlow網路,您必須首先將其轉換為UFF格式。
關於此任務
以下步驟在C ++中為TensorFlow網路添加了一個自定義外掛層:
程式

  1. 示例:使用C++新增自定義層所示,實現IPluginV2IPluginCreator類。
  2. 將TensorFlow操作對映到外掛操作。 您可以為此使用GraphSurgeon。例如,請參考以下程式碼片段以將TensorFlowRelu6操作對映到外掛:
import graphsurgeon as gs
my_relu6 = gs.create_plugin_node(name=”MyRelu6”, op=”Clip_TRT”, clipMin=0.0, clipMax=6.0)
Namespace_plugin_map = { “tf_relu6” : my_relu6 }
def preprocess(dynamic_graph):
  dynamic_graph.collapse_namespaces(namespace_plugin_map)

在上面的程式碼中,tf_relu6是TensorFlow圖中Relu6節點的名稱。 它將tf_relu6節點對映到具有“ Clip_TRT”操作的自定義外掛節點,該操作是要使用的外掛的名稱。 將上面的程式碼儲存到名為config.py的檔案中。 如果外掛層需要引數,則應將其作為引數傳遞給gs.create_plugin_node。 在這種情況下,clipMinclipMax是clip外掛所需的引數。
3. 呼叫帶有預處理-p標誌設定的UFF轉換器:

convert-to-uff frozen_inference_graph.pb -p config.py -t

這將生成一個用TensorRT外掛節點替換的TensorFlow操作的UFF檔案。
4. 使用UFF解析器使用TensorRT執行經過預處理和轉換的UFF檔案。 有關詳細資訊,請參閱從框架匯入模型時使用自定義層

位於GitHub儲存庫中的TensorFlow SSD網路(sampleUffSSD)進行物件檢測,說明了如何使用C ++新增UFF不支援的自定義層。 請參閱示例資料夾中的config.py,以獲取有關如何預處理圖形的演示。

4.1.4 示例:使用C ++新增具有動態形狀支援的自定義圖層

為了支援動態形狀,您的外掛必須派生自IPluginV2DynamicExt。 工廠/建立者和登錄檔部分與示例1類似,因此此處將不再重複這些步驟。
關於此任務
BarPlugin是一個具有兩個輸入和兩個輸出的外掛,其中:

  • 第一個輸出是第二個輸入的副本
  • 第二個輸出是兩個輸入的並置,沿著第一維,所有型別/格式必須相同且為線性格式

BarPlugin需要如下匯出:

class BarPlugin : public IPluginV2DynamicExt
{
	...override virtual methods inherited from IPluginV2DynamicExt.
};

繼承的方法都是純虛擬方法,因此,如果您忘記了一種方法,編譯器會提醒您。

受動態形狀影響的四種方法是:

  • getOutputDimensions
  • supportsFormatCombination
  • configurePlugin
  • enqueue
    getOutputDimensions的重寫根據輸入尺寸返回輸出尺寸的符號表達式。 使用傳遞給getOutputDimensions的IExprBuilder從輸入的表示式構建表示式。 在此示例中,第二個輸出的尺寸與第一個輸入的尺寸相同,因此不必為情況1構建新的表示式。
DimsExprs BarPlugin::getOutputDimensions(int outputIndex, 
    const DimsExprs* inputs, int nbInputs, 
    IExprBuilder& exprBuilder)
{
    switch (outputIndex)
    {
    case 0: 
    {
        // First dimension of output is sum of input 
        // first dimensions.
        DimsExprs output(inputs[0]);
        output.d[0] = 
            exprBuilder.operation(DimensionOperation::kSUM, 
                inputs[0].d[0], inputs[1].d[0]);
	   return output;
    }
    case 1:
        return inputs[0];
    default:
         throw std::invalid_argument(“invalid output”);
}

SupportsFormatCombination的替代項必須指示是否允許格式組合。 介面將輸入/輸出統一索引為“連線”,第一個輸入從0開始,然後依次輸入其餘輸入,然後對輸出編號。 在此示例中,輸入是連線0和1,輸出是連線2和3。

TensorRT使用supportsFormatCombination來詢問給定的格式/型別組合是否適合連線,給定的格式/型別用於索引較少的連線。 因此,覆蓋可以假定已經稽核了較少的索引連線,並將重點放在具有索引pos的連線上。

bool BarPlugin::supportsFormatCombination(int pos, const PluginTensorDesc* inOut, int nbInputs, int nbOutputs) override
{
    assert(0 <= pos && pos < 4);
    const auto* in = inOut;
    const auto* out = inOut + nbInputs;
    switch (pos)
    {
    case 0: in[0].format == TensorFormat::kLINEAR;
    case 1: return in[1].type == in[0].type &&
                   in[0].format == TensorFormat::kLINEAR;
    case 2: return out[0].type == in[0].type &&
                   out[0].format == TensorFormat::kLINEAR;
    case 3: return out[1].type == in[0].type &&
                   out[1].format == TensorFormat::kLINEAR;
    }
    throw std::invalid_argument(“invalid connection number”);
}

此處的in和out區域性變數允許通過輸入或輸出號而不是連線號來檢查inOut。
重要說明:覆蓋可能會檢查索引小於pos的連線的格式/型別,但絕不能檢查索引大於pos的連線的格式/型別。 該示例使用案例3對照連線0檢查連線3,而不使用案例0對照連線3檢查連線0。
TensorRT使用configurePlugin在執行時設定外掛。 我們的外掛不需要configurePlugin即可執行任何操作,因此它是無操作的:

void BarPlugin::configurePlugin(
    const DynamicPluginTensorDesc* in, int nbInputs, 
    const DynamicPluginTensorDesc* out, int nbOutputs) override
{
}

如果外掛需要知道它可能遇到的最小或最大尺寸,則可以檢查欄位DynamicPluginTensorDesc :: minDynamicPluginTensorDesc :: max是否有輸入或輸出。 格式和構建時維度資訊可以在DynamicPluginTensorDesc :: desc中找到。 任何執行時維都將顯示為-1。 實際尺寸提供給BarPlugin :: enqueue

最後,重寫BarPlugin :: enqueue必須完成工作。 由於形狀是動態的,因此排隊時會收到一個PluginTensorDesc,它描述每個輸入和輸出的實際尺寸,型別和格式。

4.1.5 示例:使用C ++新增具有INT8 I / O支援的自定義層

為了支援INT8 I / O,您的外掛可以從IPluginV2IOExtIPluginV2DynamicExt派生。
常規步驟與示例1:使用C ++為Caffe新增自定義圖層以及示例3:使用C ++新增具有動態形狀支援的自定義圖層相似,因此此處將不顯示重複的部分(工廠/建立者和登錄檔)。

UffPoolPluginV2是一個外掛,用於演示如何為自定義池層擴充套件INT8 I / O。 推導如下:

class UffPoolPluginV2 : public IPluginV2IOExt
{
    ...override virtual methods inherited from IPluginV2IOExt.
};

大多數純虛擬方法是外掛通用的。 影響INT8 I / O的主要方法是:

  • supportsFormatCombination
  • configurePlugin
  • enqueue
    SupportsFormatCombination的替代項必須指示允許使用哪種INT8 I / O組合。 此介面的用法類似於示例3:使用C ++新增具有動態形狀支援的自定義層。 在此示例中,支援的I / O張量格式為線性CHW,但不包括INT32,但I / O張量必須具有相同的資料型別。
bool UffPoolPluginV2::supportsFormatCombination(int pos, const PluginTensorDesc* inOut, int nbInputs, int nbOutputs) const override
{
    assert(nbInputs == 1 && nbOutputs == 1 && pos < nbInputs + nbOutputs);
    bool condition = inOut[pos].format == TensorFormat::kLINEAR;
    condition &= inOut[pos].type != DataType::kINT32;
    condition &= inOut[pos].type == inOut[0].type;
    return condition;
}

重要說明

  • 如果必須將INT8自動校準用於具有INT8 I / O外掛的網路,則該外掛應支援FP32 I / O變數,因為FP32校準圖使用了該變數。
  • 如果不支援FP32 I / O變數,或者不使用INT8自動校準,則應明確設定所有必需的INT8 I / O張量標度。
  • 自動校準不會為外掛內部張量生成動態範圍。 INT8 I / O外掛應為內部張量計算自己的每個張量動態範圍,以進行量化或反量化。

TensorRT呼叫configurePlugin方法,以通過PluginTensorDesc將資訊傳遞給外掛,該資訊儲存為成員變數,進行序列化和反序列化。

void UffPoolPluginV2::configurePlugin(const PluginTensorDesc* in, int nbInput, const PluginTensorDesc* out, int nbOutput)
{
    ...
    mPoolingParams.mC = mInputDims.d[0];
    mPoolingParams.mH = mInputDims.d[1];
    mPoolingParams.mW = mInputDims.d[2];
    mPoolingParams.mP = mOutputDims.d[1];
    mPoolingParams.mQ = mOutputDims.d[2];
    mInHostScale = in[0].scale >= 0.0f ? in[0].scale : -1.0f;
    mOutHostScale = out[0].scale >= 0.0f ? out[0].scale : -1.0f;
}

可以從PluginTensorDesc :: scale獲得每個張量的INT8 I / O標度。

最後,重寫UffPoolPluginV2 :: enqueue必須完成工作。 它包括一組核心演算法,可通過使用實際批處理大小,輸入,輸出,cuDNN流和配置的資訊在執行時執行自定義層。

int UffPoolPluginV2::enqueue(int batchSize, const void* const* inputs, void** outputs, void* workspace, cudaStream_t stream)
{
    ...
    CHECK(cudnnPoolingForward(mCudnn, mPoolingDesc, &kONE, mSrcDescriptor, input, &kZERO, mDstDescriptor, output));
    ...
    return 0;
}

4.1.6 示例:使用C ++ API實現GELU運算子

要實現GELU運算子,我們需要在網路中新增一組ElementWise和Unary層。
GELU方程為:
GELU(x)=0.5x(1+tanh[√2/π(x+0.044715x3)])

  1. 準備常數值
const float f3 = 3.0f;
const float x3Coeff = 0.044715f;
const float sqrt2OverPi = 0.7978846f;
const float f1 = 1.0f;
const float f05 = 0.5f;
  1. 實施GELU運算子
auto dim = nvinfer1::Dims3{1, 1, 1};
// y = x ^ 3
auto c3 = network->addConstant(dim, Weights{DataType::kFLOAT, &f3, 1});
auto pow1 = network->addElementWise(*x->getOutput(0), *c3->getOutput(0), ElementWiseOperation::kPOW);
// y = y * 0.044715f
auto cX3Coeff = network->addConstant(dim, Weights{DataType::kFLOAT, &x3Coeff, 1});
auto mul1 = network->addElementWise(
    *pow1->getOutput(0), *cX3Coeff->getOutput(0), ElementWiseOperation::kPROD);
// y = y + x
auto add1 = network->addElementWise(*mul1->getOutput(0), *x->getOutput(0), ElementWiseOperation::kSUM);
// y = y * 0.7978846f
auto cSqrt2OverPi = network->addConstant(dim, Weights{DataType::kFLOAT, &sqrt2OverPi, 1});
auto mul2 = network->addElementWise(*add1->getOutput(0), *cSqrt2OverPi->getOutput(0), ElementWiseOperation::kPROD);
// y = tanh(y)
auto tanh1 = network->addActivation(*mul2->getOutput(0), ActivationType::kTANH);
// y = y + 1
auto c1 = network->addConstant(dim, Weights{DataType::kFLOAT, &f1, 1});
auto add2 = network->addElementWise(*tanh1->getOutput(0), *c1->getOutput(0), ElementWiseOperation::kSUM);
// y = y * 0.5
auto c05 = network->addConstant(dim, Weights{DataType::kFLOAT, &f05, 1});
auto mul3 = network->addElementWise(*add2->getOutput(0), *c05->getOutput(0), ElementWiseOperation::kPROD);
// y = y * x
auto y = network->addElementWise(*mul3->getOutput(0), *x->getOutput(0), ElementWiseOperation::kPROD);

注意:考慮到GELU不是線性函式,在將網路設定為以INT8模式執行時,將每一層的精度設定為FP32。
有關與GELU相關的圖層融合的更多資訊,請參見TensorRT最佳實踐指南

4.2 使用Python API新增自定義圖層

儘管C ++ API是實現自定義圖層的首選語言; 由於可以輕鬆訪問CUDA和cuDNN等庫,因此您還可以在Python應用程式中使用自定義層。
您可以使用C ++ API建立自定義圖層,使用pybind11在Python中打包該圖層,然後將該外掛載入到Python應用程式中。 有關更多資訊,請參見在Python中建立網路定義

相同的自定義層實現可用於C ++和Python。

4.2.1 示例:使用Python向TensorRT網路新增自定義層

可以使用外掛節點將自定義層新增到Python中的任何TensorRT網路中。
Python API具有一個名為add_plugin_v2的函式,該函式使您可以將外掛節點新增到網路。 以下示例說明了這一點。 它建立一個簡單的TensorRT網路,並通過查詢TensorRT Plugin Registry新增一個Leaky ReLU外掛節點。

import tensorrt as trt
import numpy as np

TRT_LOGGER = trt.Logger()

trt.init_libnvinfer_plugins(TRT_LOGGER, '')
PLUGIN_CREATORS = trt.get_plugin_registry().plugin_creator_list

def get_trt_plugin(plugin_name):
        plugin = None
        for plugin_creator in PLUGIN_CREATORS:
            if plugin_creator.name == plugin_name:
                lrelu_slope_field = trt.PluginField("neg_slope", np.array([0.1], dtype=np.float32), trt.PluginFieldType.FLOAT32)
                field_collection = trt.PluginFieldCollection([lrelu_slope_field])
                plugin = plugin_creator.create_plugin(name=plugin_name, field_collection=field_collection)
        return plugin

def main():
    with trt.Builder(TRT_LOGGER) as builder, builder.create_network() as network:
        builder.max_workspace_size = 2**20
        input_layer = network.add_input(name="input_layer", dtype=trt.float32, shape=(1, 1))
        lrelu = network.add_plugin_v2(inputs=[input_layer], plugin=get_trt_plugin("LReLU_TRT"))
        lrelu.get_output(0).name = "outputs"
        network.mark_output(lrelu.get_output(0))

4.2.2 示例:使用Python新增UFF不支援的自定義層

TensorFlow網路可以轉換為UFF格式,並通過Python介面與TensorRT一起執行。
關於此任務
為此,我們使用了GraphSurgeon API。 如果要編寫自己的外掛,則需要通過實現IPluginExt和IPluginCreator類在C ++中實現,如示例:使用C ++新增自定義層所示
以下步驟說明了如何使用UFF解析器通過在TensorRT外掛登錄檔中註冊的外掛節點來執行自定義層。
程式

  1. 通過呼叫**trt.init_libnvinfer_plugins(TRT_LOGGER,’’)**註冊TensorRT外掛(或在註冊了自己的外掛的地方載入.so檔案)。
  2. 準備網路並檢查TensorFlow輸出:
tf_sess = tf.InteractiveSession()
tf_input = tf.placeholder(tf.float32, name="placeholder")
tf_lrelu = tf.nn.leaky_relu(tf_input, alpha=lrelu_alpha, name="tf_lrelu")
tf_result = tf_sess.run(tf_lrelu, feed_dict={tf_input: lrelu_args})
tf_sess.close()
  1. 準備名稱空間對映。 操作名稱LReLU_TRT對應於TensorRT附帶的Leaky ReLU外掛。
trt_lrelu = gs.create_plugin_node(name="trt_lrelu", op="LReLU_TRT", negSlope=lrelu_alpha)
namespace_plugin_map = {
            "tf_lrelu": trt_lrelu
 }
  1. 使用GraphSurgeon轉換TensorFlow圖並儲存到UFF:
dynamic_graph = gs.DynamicGraph(tf_lrelu.graph)
dynamic_graph.collapse_namespaces(namespace_plugin_map)
  1. 執行UFF解析器並將結果與TensorFlow進行比較:
uff_model = uff.from_tensorflow(dynamic_graph.as_graph_def(), ["trt_lrelu"], output_filename=model_path, text=True)
parser = trt.UffParser()
parser.register_input("placeholder", [lrelu_args.size])
parser.register_output("trt_lrelu")
parser.parse(model_path, trt_network)

有關更多資訊,請參閱在Python的TensorRT中為您的TensorFlow網路新增自定義層(uff_custom_plugin)示例。

4.3 從框架匯入模型時使用自定義層

TensorRT解析器使用層操作欄位來識別網路中的特定層是否為TensorFlow支援的操作。
TensorFlow
與TensorRT的先前版本相比,TensorRT UFF解析器如何執行TensorFlow中的自定義層有一些變化。對於TensorFlow模型,請使用UFF轉換器將圖形轉換為UFF檔案。在此過程中,如果網路包含外掛層,則還必須將這些層的操作欄位對映到TensorRT中相應的已註冊外掛名稱。這些外掛可以是TensorRT附帶的外掛,也可以是您編寫的自定義外掛。網路中的外掛欄位名稱也應與外掛期望的欄位匹配。可以使用GraphSurgeon完成此操作,如使用Graph Surgeon API預處理TensorFlow圖表中所述,以及如GitHub儲存庫中的TensorFlow SSD網路的物件檢測(sampleUffSSD)中所演示的那樣,通過將配置檔案與UFF轉換器一起使用。

UFF解析器將為每個不受支援的操作查詢外掛登錄檔。如果發現與任何已註冊外掛名稱匹配,則解析器將從輸入網路中解析外掛欄位引數,並使用它們建立外掛物件。然後將該物件新增到網路。在TensorRT的早期版本中,您必須實現nvuffparser :: IPluginFactoryExt並將外掛引數手動傳遞給**createPlugin(…)**函式。儘管仍然可以執行此流程,但是對於外掛API的新新增不再需要。有關更多資訊,請參見:

Caffe
對於Caffe模型,請使用nvcaffeparser1 :: IPluginFactoryV2類。解析器的setPluginFactoryV2方法在解析器中設定工廠以啟用自定義圖層。在解析模型描述時,解析器針對每一層呼叫isPluginV2,以與工廠核對層名稱是否對應於自定義層。如果是,則解析器使用層的名稱例項化呼叫createPlugin的外掛(以便工廠可以例項化相應的外掛),Weights陣列和權重數作為引數。如果單個工廠與不同的層名稱相關聯,則對單個工廠可以支援的外掛數量沒有限制。
注意:對於Caffe解析器,如果使用setPluginFactoryV2IPluginFactoryV2,則在反序列化期間建立的外掛物件將由引擎在內部通過呼叫IPluginExt :: destroy()銷燬。您僅負責銷燬在網路建立步驟中建立的外掛物件,如使用C ++ API新增自定義層所示。
位於GitHub儲存庫中的在TensorRT(samplePlugin)示例中向網路新增自定義層說明了如何擴充套件
nvcaffeparser1 :: IPluginFactoryExt
以使用自定義層,而使用TensorFlow SSD網路(sampleUffSSD)的物件檢測使用UFF解析器使用自定義圖層。

ONNX
對於ONNX模型,ONNX解析器將自動嘗試將無法識別的操作作為外掛匯入。如果在登錄檔中找到與節點具有相同op_type的外掛,則解析器將解析ONNX模型中的外掛欄位引數,並使用相應的建立者來建立外掛例項。預設情況下,它將嘗試載入外掛版本1。可以通過在相應的ONNX節點中設定plugin_version字串引數來覆蓋此行為。

在某些情況下,您可能需要先修改ONNX圖,然後再將其匯入TensorRT。例如,新增上述的plugin_version屬性,或用外掛節點替換一組操作。為此,您可以使用ONNX GraphSurgeon實用程式。

有關TensorRT的自定義層在Python中的用法,請參考:

4.3.1 示例:向TensorFlow模型新增自定義層

為了使用TensorRT執行TensorFlow網路,您必須首先將其轉換為UFF格式。在轉換過程中,可以使用graphsurgeon實用程式將自定義層標記為外掛節點。
然後,UFF轉換器將處理後的圖形轉換為UFF格式,然後由UFF解析器執行。然後,由UFF Parser將外掛節點新增到TensorRT網路。

有關使用C ++ API的詳細資訊,請參見示例:使用C ++新增UFF不支援的自定義層。

有關使用Python API的詳細資訊,請參見示例2:使用Python新增UFF不支援的自定義層。此外,Python中使用SSD進行物件檢測(uff_ssd)示例演示了Python中用於執行TensorFlow物件的端到端工作流程

4.4 外掛API說明

所有新外掛都應從IPluginCreator使用C ++ API新增自定義層中所述的外掛基類之一派生類。 此外,新外掛還應呼叫**REGISTER_TENSORRT_PLUGIN(…)巨集,以在TensorRT外掛登錄檔中註冊外掛,或建立等效於initLibNvInferPlugins()**的初始化函式。

4.4.1 將外掛從TensorRT 6.x.x遷移到TensorRT 7.x.x

雖然仍然支援IPluginV2和IPluginV2Ext介面以分別與TensorRT 5.1和6.0.x向後相容,但我們建議您編寫新外掛或重構現有外掛以定位IPluginV2DynamicExtIPluginV2IOExt介面,如第4.1節所述。
為了使用最新的外掛層功能,您的自定義外掛應實現IPluginV2DynamicExtIPluginV2IOExt介面。

IPluginV2DynamicExt中的新功能如下:

virtual DimsExprs getOutputDimensions(int outputIndex, const DimsExprs* inputs, int nbInputs, IExprBuilder& exprBuilder) = 0;

virtual bool supportsFormatCombination(int pos, const PluginTensorDesc* inOut, int nbInputs, int nbOutputs) = 0;

virtual void configurePlugin(const DynamicPluginTensorDesc* in, int nbInputs, const DynamicPluginTensorDesc* out, int nbOutputs) = 0;

virtual size_t getWorkspaceSize(const PluginTensorDesc* inputs, int nbInputs, const PluginTensorDesc* outputs, int nbOutputs) const = 0;

virtual int enqueue(const PluginTensorDesc* inputDesc, const PluginTensorDesc* outputDesc, const void* const* inputs, void* const* outputs, void* workspace, cudaStream_t stream) = 0;

IPluginV2IOExt中的新功能如下:

virtual void configurePlugin(const PluginTensorDesc* in, int nbInput, const PluginTensorDesc* out, int nbOutput) = 0;

virtual bool supportsFormatCombination(int pos, const PluginTensorDesc* inOut, int nbInputs, int nbOutputs) const = 0;

遷移到IPluginV2DynamicExtIPluginV2IOExt的準則:

  • getOutputDimensions實現給定輸入的輸出張量尺寸的表示式。
  • supportsFormatCombination檢查外掛是否支援指定輸入/輸出的格式和資料型別。
  • configurePlugin模擬IPluginV2Ext中等效的configurePlugin的行為,但接受張量描述符。
  • getWorkspaceSize和enqueue模仿IPluginV2Ext中等效API的行為,但接受張量描述符。
  • 有關API的更多詳細資訊,請參見IPluginV2 API說明中的API說明。
    有關API的更多詳細資訊,請參見IPluginV2 API說明中的API說明。

4.4.1.1 將外掛從TensorRT 5.x.x遷移到TensorRT 6.x.x

仍然支援IPluginV2介面,但是,我們建議您使用IPluginV2Ext介面編寫新的外掛,並將所有現有的外掛實現遷移到IPluginV2Ext介面。
為了使用最新的外掛層功能,您的自定義外掛應實現IPluginV2Ext介面。 新功能如下:

virtual nvinfer1::DataType getOutputDataType(int index, const nvinfer1::DataType* inputTypes, int nbInputs) const = 0;


virtual bool isOutputBroadcastAcrossBatch(int outputIndex, const bool* inputIsBroadcasted, int nbInputs) const = 0;

virtual bool canBroadcastInputAcrossBatch(int inputIndex) const = 0;


virtual void configurePlugin(const Dims* inputDims, int nbInputs, const Dims* outputDims,
            int nbOutputs, const DataType* inputTypes, const DataType* outputTypes, const bool* inputIsBroadcast, const bool* outputIsBroadcast, PluginFormat floatFormat, int maxBatchSize) = 0;

對於最簡單的遷移,請遵循以下準則:

  • 如果層沒有輸入,則getOutputDataType可以返回輸入的型別(來自inputTypes)或DataType :: kFLOAT
  • 如果外掛不支援輸出廣播,則isOutputBroadcastAcrossBatch可以返回false。
  • 如果外掛無法處理廣播的輸入,則canBroadcastInputAcrossBatch可以返回false。
  • configurePlugin可以模仿configureWithFormat的行為。
    有關API的詳細資訊,請參見IPluginV2 API說明中的API說明

IPluginV2 API說明
下一節描述IPluginV2類的功能。要將外掛層連線到相鄰層並設定輸入和輸出資料結構,構建器將通過呼叫以下外掛方法來檢查輸出數量及其尺寸。
getNbOutputs
用於指定輸出張量的數量。

getOutputDimensions
用於指定輸出尺寸作為輸入尺寸的函式。

supportFormat
用於檢查外掛是否支援給定的資料格式。

getOutputDataType
用於獲取給定索引處輸出的資料型別。返回的資料型別必須具有外掛支援的格式。

外掛層可以支援四種資料格式和佈局,例如:
NCHW單(FP32),半精度(FP16)和整數(INT32)張量
NC / 2HW2NHWC8半精度(FP16)張量

格式由PluginFormatType列舉。
不需要在原位計算所有資料並且需要輸入和輸出張量之外還需要記憶體空間的外掛可以使用getWorkspaceSize方法指定其他記憶體需求,此方法由構建器呼叫,以確定並預分配暫存空間。

在構建和推斷期間,都可能多次配置和執行外掛層。在構建時,為了發現最佳配置,需要配置,初始化,執行和終止該層。為外掛選擇最佳格式後,將再次對其進行配置,然後將其初始化一次,並在推理應用程式的生命週期內根據需要執行多次,最後在引擎被銷燬時終止。這些步驟由構建器和引擎使用以下外掛方法控制:
configurePlugin
交流輸入和輸出的數量,所有輸入和輸出的尺寸和資料型別,所有輸入和輸出的廣播資訊,所選外掛格式以及最大批處理大小。此時,外掛將設定其內部狀態,併為給定配置選擇最合適的演算法和資料結構。

initialize
此時的配置是已知的,並且正在建立推理引擎,因此外掛可以設定其內部資料結構並準備執行。

enqueue
封裝外掛的實際演算法和核心呼叫,並提供執行時批處理大小,指向輸入,輸出和暫存空間的指標以及用於核心執行的CUDA流。

terminate
引擎上下文被破壞,外掛所擁有的所有資源都應釋放。

clone
每當建立包含此外掛層的新構建器,網路或引擎時,都會呼叫此方法。它應該返回帶有正確引數的新外掛物件。

destroy
用於破壞每次建立新外掛物件時分配的外掛物件和/或其他記憶體。每當構建器,網路或引擎被銷燬時,都會呼叫它。

set/getPluginNamespace
此方法用於設定此外掛物件所屬的庫名稱空間(預設可以為“”)。來自同一外掛庫的所有外掛物件應具有相同的名稱空間。

IPluginV2Ext支援可以處理廣播輸入和輸出的外掛。為此功能需要實現以下方法:
canBroadcastInputAcrossBatch
對於跨張量在語義上廣播的每個輸入,都會呼叫此方法。如果canBroadcastInputAcrossBatch返回true(表示外掛可以支援廣播),則TensorRT將不會複製輸入張量。外掛將在整個批次中共享一個副本。如果返回false,TensorRT將複製輸入張量,使其看起來像未經廣播的張量。

isOutputBroadcastAcrossBatch
每個輸出索引都呼叫此方法。外掛應在給定索引處返回true輸出,並在批處理中廣播。

IPluginV2IOExt
構建器在initialize()之前呼叫此方法。它為該層提供了機會,可以根據I / O PluginTensorDesc和最大批處理大小來選擇演算法。

4.4.3 IPluginCreator API說明

IPluginCreator類中的以下方法用於從外掛登錄檔中查詢和建立適當的外掛。
getPluginName
這將返回外掛名稱,並且應與IPluginExt :: getPluginType的返回值匹配。

getPluginVersion
返回外掛版本。對於所有內部TensorRT外掛,此預設值為1。

getFieldNames
為了成功建立外掛,必須知道外掛的所有欄位引數。此方法返回PluginFieldCollection結構,其中填充了PluginField條目以反映欄位名稱和PluginFieldType(資料應指向nullptr)。

createPlugin
此方法用於使用PluginFieldCollection引數建立外掛。應該填充PluginField條目的資料欄位,以指向每個外掛欄位條目的實際資料。

deserializePlugin
TensorRT引擎會根據外掛名稱和版本在內部呼叫此方法。它應該返回用於推理的外掛物件。

set / getPluginNamespace
此方法用於設定此建立者例項所屬的名稱空間(預設可以為“”)。

4.4.4 永續性LSTM外掛

以下部分描述了新的Persistent LSTM外掛。持久LSTM外掛支援半精度持久LSTM。要在網路中建立一個永續性LSTM外掛,您需要呼叫:

auto creator= getPluginRegistry()-> getPluginCreator(“ CgPersistentLSTMPlugin_TRT”,“ 1”)

IPluginV2 * cgPersistentLSTMPlugin = creator-> createPlugin(“ CgPersistentLSTMPlugin_TRT”,&fc);

fc是一個由4個引數組成的PluginField陣列:

  • hiddenSize:這是一個INT32引數,它指定LSTM的隱藏大小。
  • numLayers:這是一個INT32引數,用於指定LSTM中的層數。
  • bidirectionFactor:這是一個INT32引數,指示LSTM是否是雙向的。如果LSTM是雙向的,則該值應設定為2,否則,該值應設定為1。
  • setInitialStates:這是一個INT32引數,指示LSTM是否具有初始狀態和單元格值作為輸入。如果將其設定為0,則初始狀態和單元格值將為零。建議使用此標誌,而不是提供零狀態和單元格值作為輸入以獲得更好的效能。

可以通過呼叫以下命令將外掛新增到網路:

auto lstmLayer =網路-> addPluginV2(&inputs [0]6* cgPersistentLSTMPlugin);

輸入是具有6個元素的ITensor指標的向量,其順序如下:

  1. input:這些是LSTM的輸入序列。
  2. seqLenTensor:這是序列長度向量,用於儲存每個序列的有效長度。
  3. weight:此張量包含LSTM所需的所有重量。即使該張量為1D,也可以使用以下3D索引[isW,layerNb,gateType]進行檢視。 isW從假到真開始,表明權重的前一半是迴圈權重,後一半是輸入權重。 layerNb從0到numLayers * bidirectionalFactor開始,使得第一層是實際層的正向,第二層是反向。 gateType遵循以下順序:輸入,單元格,忘記和輸出。
  4. bias:類似於重量,此張量包含LSTM所需的所有偏差。即使該張量為1D,也可以使用以下3D索引[layerNb,isW,gateType]進行檢視。注意偏差和重量之間的細微差別。
  5. initial hidden state:如果setInitialStates為0,則指標應設定為null。否則,張量應包含初始隱藏狀態值和以下座標[批處理索引,layerNb,隱藏索引]。批處理索引指示批處理內的索引,隱藏索引是對hiddenSize長度向量的索引。
  6. initial cell state:如果setInitialStates為0,則指標應設定為null。否則,張量應包含初始隱藏狀態值和以下座標[批處理索引,layerNb,隱藏索引]。

4.5 自定義圖層外掛的最佳做法

轉換使用者定義的圖層
要將自定義層實現建立為TensorRT外掛,您需要為外掛實現IPluginV2Ext類和IPluginCreator類。

有關這兩個API類的更多資訊,請參見外掛API描述

對於Caffe網路,請參閱示例:使用C ++新增自定義層。對於TensorFlow(UFF)網路,請參閱示例:使用C ++新增UFF不支援的自定義層。

使用UFF外掛API
有關如何在C ++和Python中將外掛與UFF一起使用的示例,請參見示例:使用C ++新增自定義層示例:使用Python新增UFF不支援的自定義層。

除錯自定義層問題
必須釋放外掛中分配的記憶體,以確保沒有記憶體洩漏。如果在**initialize()函式中獲取了資源,則需要在Terminate()**函式中釋放它們。最好在外掛類的解構函式中或銷燬中釋放所有其他記憶體分配