TensorRT Plugin的实现、调试与验证:以实现Layernorm为例
一、Plugin是什么
TensorRT的Plugin直译应该为插件,望文生义的解释是插入到作为一个计算节点插入到TensorRT构造的计算图中,所以Plugin本质上可以理解为一个计算节点,会以动态链接库的形式插入到网络模型中实现某些算子。
二、什么时候需要Plugin
为了实现以下几种需求时:
- 实现TensorRT原生不支持的层或者结构
- 替换TensorRT原本实现性能不够好的层或者结构
- 手动合并没有自动融合的层或者结构
三、Plugin的特点
- 自定义cuda kerkel,功能和性能实现完全自定义
- Plugin实现的算子不可以与其他Layer之间融合,如果Plugin覆盖的结构中有参与算子融合的结构,那么此处算子融合会被破坏。
- Plugin节点前后可能会插入reformatting节点,增加开销。
四、onnx计算节点与TensorRT Plugin之间的联系
源码地址: https://github.com/onnx/onnx-tensorrt/blob/main/builtin_op_importers.cpp#L4881
onnx-parser
的源码可知,对于一个onnx中的计算节点,当解析器中其他的解析规则都不满足时会转向使用TensorRT Plugin构造,onnx中计算节点的参数会成为TensorRT Plugin中构造时的传参。按照源码里面的要求,假设我们想对一个onnx计算节点编写Plugin,那么Plugin的名字就要与onnx计算节点的名字相同,且版本为"1"
。
使用技巧: 假设我们想融合onnx中一些节点的操作,比如LayerNorm,那么就可以对Layernorm牵扯的节点的参数收集起来,然后删除这些节点,用新的名字为“Layernorm”的节点来替换这些节点,接着写TensorRT Plugin,名字为“Layernorm”,来实现上述的融合操作。
五、Plugin的实现流程
5.1 Plugin的种类
我们可以通过实现Plugin基类定义好的接口(虚函数)来自定义Plugin。 根据继承基类的种类,可以将Plugin的种类分为三种
Introduced in TensorRT version? | Mixed I/O formats/types | Dynamic shapes? | Supports implicit/explicit batch mode? | |
---|---|---|---|---|
IPluginV2Ext | 5.1 | Limited | No | Both implicit and explicit batch modes |
IPluginV2IOExt | 6.0.1 | General | No | Both implicit and explicit batch modes |
IPluginV2DynamicExt | 6.0.1 | General | Yes | Explicit batch mode only |
上面的表格抄录自官方文档,作者写这篇文章时,TensorRT版本已经升到8.4,所以Plugin的实现通常就是继承表格中最后一行的IPluginV2DynamicExt
类,也就是可以实现动态shape的。
5.2 Plugin的定义实现相关接口
Plugin的实现步骤:
- 继承
IPluginV2DynamicExt
类实现一个新的Plugin类 - 继承
IPluginCreator
类实现一个PluginCreator类 - 实现用于计算的CUDA C++ kernel
- 将Plugin编译为动态链接库.so
- c++/python代码中加载so文件
Plugin构建期和运行期要执行的交互(下面所述功能均反映在IPluginV2DynamicExt的接口定义上) 构建期
- 用户或者解析器需要向Plugin传递构造参数和权重
- Plugin需要报告其支持的输入输出张量信息(包括类型,形状、数量、数据分布等)
- Plugin需要报告所需的workspace大小
- TensorRT会尝试所有允许的组合,然后选择性能最佳的组合(可能会在Plugin前后插入reformat节点)
- Plugin要实现序列化和反序列化接口
运行期
- TensorRT为Plugin提供输入输出张量的所有信息,workspace地址还有cuda stream
注意:不要在运行期enqueue
函数里面使用cudaMalloc
等耗时的函数
Plugin需要实现的关键API
- getOutputDimensions
根据输入张量形状,向 TensorRT 报告每个输出张量的形状
- supportsFormatCombination
向TensorRT报告支持的数据类型和数据排布 Plugin 可以同时支持多种数据类型(DataType)和数据排布(LayerOut)的输入输出张量组合 TensorRT 深度优先遍历各个输入张量索引,尝试各种组合,该函数返回“Plugin 是否支持当前尝试的组合” 保证性能的前提下, 多实现一些 format 的组合, 消除 TenosrRT 自动插入 reformat 节点的开销
-
configurePlugin
-
在推理前将调用该成员函数
-
Dynamic Shape 模式中,每当输入数据形状发生变化(调用 context.set_binding_shape)时,该成员函数被调用
-
构建期调用时 in/out 张量形状中含有 -1
-
运行期调用时 in/out 张量形状为真实绑定的形状
-
getWorkspaceSize
-
向 TensorRT 报告中间计算结果的存储空间
-
workspace显存申请和释放由 TensorRT 管理
-
enqueue
-
调用 CUDA C++ kernel 计算的地方
-
可以根据输入张量的不同形状、数据类型等条件选择不同 kernel 执行计算
-
不要在 enqueue 中使用 cudaMalloc* 等函数
-
资源管理类
-
initialize(engine 创建时被调用,用于初始化 Plugin 层)
-
terminate (engine 销毁时被调用,用于释放 initialize 函数申请的资源)
-
clone(创建多个 context ,可以与源对象共享本 engine 的资源)
-
attachToContext(申请使用 context 独占的 cudnn 或 cublas 资源)
-
detachFromContext(销毁 context 独占的 cudnn 或 cublas 资源)
-
destroy(当 context/engine 销毁时被调用)
-
序列化反序列化
-
序列化(Plugin 负责)
-
getSerializationSize(报告序列化需要的空间大小,单位 Byte)
-
serialize(将Plugin 数据序列化到给定的 buffer 中)
-
-
反序列化(PluginCreator 负责)
-
deserializePlugin(把序列化的 buffer 传给 Plugin 的构造函数)
-
Plugin 构造函数(从 buffer 中读取数据并完成 Plugin 构造)
-
PluginCreator的关键API
- createPlugin(依照传入参数调用 Plugin 构造函数)
- 注册 PluginCreator
REGISTER_TENSORRT_PLUGIN(xxxPluginCreator);
-
Version 和 namespace 相关
-
序列化时 TensorRT 会将 Plugin 的名字(Name)、类型(Type)、版本号(Version)、命名空间(Namespace)信息写入 engine
-
通常不用修改
5.3 Plugin的功能实现技巧
- 先保证原生计算的正确性
- 尝试采用或者基于自带Plugin修改
- 尝试采用其他框架的cuda实现
- 采用cublas和cudnn自行实现
- 采用原生cuda自行编写
六、LayerNorm的原理与Plugin的实现
开源地址:
https://github.com/thb1314/tensorrt-layernorm-plugin
版本要求: TensorRT8.x
作者实现的Layernorm插件性能是有保证的,cuba部分移植的oneflow的官方实现。
https://github.com/Oneflow-Inc/oneflow/blob/master/oneflow/core/cuda/layer_norm.cuh
移植技巧:根据cuh函数名字在github仓库中搜索具体用例,然后写一个适配器函数或者直接修改原函数接口来满足自己的使用。
Layernorm的原理
这里仅给出Layernorm的计算过程,为什么需要Layernorm请自行搜索。
torch伪代码:
拓展
1. 如果是应用在CV领域,x的shape为[B,C,H,W]的是时候应该对谁进行归一化和求mean呢?
2. torch自带的nn.LayerNorm怎么适配这种情况呢?
在插件的enqueue函数,做了如下类型判断,也就是当前的LayerNorm Plugin fp32/fp16都是支持的。
enqueue
函数内部有cudaMalloc
函数,前面不是说不能用么,这里的逻辑在configurePlugin
逻辑也有实现,目的是为了按需动态申请mean和std的显存。按理说这里的if判断是进不去的,这么写是为了安全一些。
七、Plugin的Debug技巧
如果读者由仔细阅读源码的话,可以发现里面的trt_tensor相关的类与插件编写没什么关联,放在这里是为了后续的调试。 调试的主要操作就是获取中间结果并保存然后采用其他方式验证。
- enqueue函数中间结果调试使用cnpy保存为npz文件
- 单个cuda kernel的调试建议另外起一个新项目,仿照cublas项目
八、LayerNorm的验证
LayerNorm的验证可以采用TensorRT python的API来验证。 输入输出由numpy tensor定义,具体函数实现也可以通过numpy来模拟,然后对比tensorRT的输出结果和numpy的输出结果。 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
|
九、总结
TensorRT Plugin的实现不算很复杂,接口定义也很清晰。本文详细介绍了TensorRT Plugin的基础使用和实现,学海无涯,作者会继续学习为读者分享更多关于Plugin的高级用法。
创建日期: September 17, 2024