TensorRT笔记(5)使用自定义层扩展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()**函数中释放它们。最好在插件类的析构函数中或销毁中释放所有其他内存分配

猜你喜欢

转载自blog.csdn.net/qq_33287871/article/details/113731432
今日推荐