TensorRT基础入门(二)
本文属于学习笔记,在重点章节或代码位置加入个人理解,欢迎批评指正!
参考
CUDA与TensorRT部署部署实战第三章
六. onnx注册算子的方法
学习目标
- 学习pytorch导出onnx不成功的时候如何解决(无plugin版本)
6.0 当导出onnx出错时,如何思考?
难度有易到难:
1)修改onnx opset版本
2)若不考虑自己写tensorRT plugin插件,怎还需考虑onnx-trt是否有算子支持
3)替换pytorch中的算子组合
4)pytorch中未登记onnx算子
5)直接修改onnx, 创建plugin, 使用onnx-surgeon
6.1 方式一
符号函数 + register_custom_op_symbolic
Eg. Asinh 函数
# 定义符号函数,参数与onnx中算子对应上
def asinh_symbolic(g, input, *, out=None):return g.op("Asinh", input) #onnx中的算子
# 注册,aten是一个实现张量运算的C++库
register_custom_op_symbolic('aten::asinh', asinh_symbolic, 12)
容易混淆的地方
- register_op中的第一个参数是PyTorch中的算子名字: aten::asinh
- g.op中的第一个参数是onnx中的算子名字: Asinh
6.2 方式二
使用类似于torch/onnx/symbolic_opset.py中的写法
# 让此算子可以与底层C++实现的aten::asinh绑定
# 一般如果这么写的话,其实可以把这个算子直接加入到torch/onnx/symbolic_opset*.py中
_onnx_symbolic = functools.partial(registration.onnx_symbolic, opset=9)
@_onnx_symbolic('aten::asinh')
def asinh_symbolic(g, input, *, out=None):return g.op("Asinh", input)
6.3 方式三
注册算子类
class CustomOp(torch.autograd.Function):@staticmethod # 自定义符号函数def symbolic(g: torch.Graph, x: torch.Value) -> torch.Value:return g.op("custom_domain::customOp2", x)@staticmethoddef forward(ctx, x: torch.Tensor) -> torch.Tensor:ctx.save_for_backward(x)x = x.clamp(min=0)return x / (1 + torch.exp(-x))customOp = CustomOp.apply
七. onnx-graph-surgeon
学习目标
- 使用onnx-graphsurgeon ,比较与onnx.helper的区别
- 学习快速修改onnx以及替换算子/创建算子的技巧,5个参考案例
7.1 onnx-graphsurgeon
是TensorRT下的一个工具包,与onnx的protobufu组织结构有区别,onnx-graphsurgeon更简单。
Eg.
1) 使用原生的gs创建一个conv
# 输入tensor ,开始推理前是不确定的input = gs.Variable(name = "input0",dtype = np.float32,shape = (1, 3, 224, 224))weight = gs.Constant(name = "conv1.weight",values = np.random.randn(5, 3, 3, 3))bias = gs.Constant(name = "conv1.bias",values = np.random.randn(5))output = gs.Variable(name = "output0",dtype = np.float32,shape = (1, 5, 224, 224))node = gs.Node(op = "Conv",inputs = [input, weight, bias],outputs = [output],attrs = {"pads":[1, 1, 1, 1]})graph = gs.Graph(nodes = [node],inputs = [input],outputs = [output])
2) gs可以自定义一些函数算子
去创建onnx,使整个onnx的创建更加方便
####在graph注册调用的函数####
@gs.Graph.register()
def add(self, a, b):return self.layer(op="Add", inputs=[a,b], outputs=["add_out_gs"]
@gs.Graph.register()
def mul(self, a, b):return self.layer(op="Mul", inputs=[a, b], outputs=["mul_out_gs"])
@gs.Graph.register()
def gemm(self, a, b, trans_a=False, trans_b=False):attrs = {"transA": int(trans_a), "transB": int(trans_b)}return self.layer(op="Gemm", inputs=[a, b], outputs=["gemm_out_gs"], attrs=attrs)
@gs.Graph.register()
def relu(self, a):return self.layer(op="Relu", inputs=[a], outputs=["act_out_gs"])#### 设计网络架构, 更简单
gemm0 = graph.gemm(input0, consA, trans_b=True)
relu0 = graph.relu(*graph.add(*gemm0, consB))
mul0 = graph.mul(*relu0, consC)
output0 = graph.add(*mul0, consD)
7.2 挖出子图,使用polygraphy分析
用途:某些网络结构复杂,想分析部分网络结构(子图)或者精度。
Eg. 从swin-tf中摘取出LayerNorm层
#LayerNorm部分print(tensors["/patch_embed/Transpose_output_0"])print(tensors["/patch_embed/norm/LayerNormalization_output_0"])# 去onnx图中的相应模块地方 找到上一个output的name 然后将这个tensor复制进来graph.inputs = [tensors["/patch_embed/Transpose_output_0"].to_variable(dtype = np.float32, shape = (1, 3136, 128))]# 同理输出graph.outputs = [tensors["/patch_embed/norm/LayerNormalization_output_0"].to_variable(dtype = np.float32, shape = (1, 3136, 128))]# ** 重要apigraph.cleanup() onnx.save(gs.export_onnx(graph), "models/LN-swin-subgraph.onnx")
7.3 替换算子
将minmax替换为clip。
#####################通过注册的clip算子替换网络节点####################
# input (5, 5)
# |
# identity
# |
# clip
# |
# identity
# |
# output (5, 5)
def change_onnx_graph():graph = gs.import_onnx(onnx.load_model('../models/smaple-minmax.onnx'))tensors = graph.tensors()inputs = [tensors["identity_output_0"],tensors["onnx_graphsurgeon_constant_5"],tensors["onnx_graphsurgeon_constant_2"]]outputs = [tensors["max_output_6"]]## 替换子网,把子网和其周围节点断开联系for items in inputs:item.outputs.clear()for item in outputs:item.inputs.clear()# 通过注册的clip,重新把断开的联系链接起来graph.clip(inputs, outputs)# 删除所有额外的节点graph.cleanup()onnx.save(gs.export_onnx(graph), "../models/sample-minmax-to-clip.onnx")
注意
:替换layerNorm算子例子参考课件工程中代码,因为低版本中此算子展开很丑陋,opset17才开始支持
八. 快速分析开源代码并导出onnx
学习目标
以swin-transformer为例,导出并分析
完整实验流程
1)opset_version 设置为9, 将export.py 导出脚本拷贝到swin-transformer官方工程目录下,执行导出脚本.
cd Swin-Transformer/
python export.py --eval --cfg configs/swin/swin_base_patch4_window7_224.yaml --resume ../weights/swin_tiny_patch4_window7_224.pth --data-path data/ --local_rank 0
参数说明:
- eval: 评估模式
- cfg: 配置文件地址
- –resume :加载预训练权重文件
- data-path: 数据集根目录路径
结果:
roll算子不支持。
torch.roll(input, shifts, dims=None) 用于沿指定维度滚动张量元素,超出边界的元素会循环填充到另一侧。
# 原始数据是4*4 的矩阵
# 分别按照行下滚动两行,右滚动两列
torch.roll(a, shifts=(2, 2), dims=(0, 1))
# 输出:
tensor([[10, 11, 8, 9],[14, 15, 12, 13],[ 2, 3, 0, 1],[ 6, 7, 4, 5]])
Tips
实际实验过程中,opset_version 设置为9, PatchMerging::downsample 算子不支持, 改为12直接成功了.
原因是本地torch版本较高1.13.1
,已经在onnx/opset_symbolic9.py中实现了roll算子。
2)opset_version 设置成12, 将自定义的roll算子放入到onnx/symbolic12.py中进行onnx算子注册,再导出。
## 在 ONNX 符号函数中,第一个参数是 g(GraphContext),第二个参数通常命名为 input 或 self(代表输入张量)。
@parse_args('v', 'is', 'is')
def roll(g, self, shifts, dims): assert len(shifts) == len(dims)result = self## roll算子 = “slice切片” + “concat” 组合形式for i in range(len(shifts)):shapes = []# - 2 表示倒数第二个元素shape = sym_help._slice_helper(g,result,axes=[dims[i]],starts=[-shifts[i]],ends=[maxsize])shapes.append(shape)shape = sym_help._slice_helper(g,result,axes=[dims[i]],starts=[0],ends=[-shifts[i]])shapes.append(shape)result = g.op("Concat", *shapes, axis_i=dims[i])return result
Tips.
layerNorm在opset17之后开始支持, onnx模型中会更简洁,TensorRT8.,6之后也支持了该算子。
九. trtexec使用
学习目标
学习使用trtexec,以及通过trtexec的日志理解TensorRT内部所做的优化
9.1 onnx导出TensorRT模型三种方式
- 使用trtexec工具(二进制文件,可修改很多参数):快速生成推理引擎
- 使用TensorRT python api
- 使用TensorRT C++ api
9.2 使用trtexec工具
1.build脚本
trtexec执行build脚本以及option参数含义, 详细可以参考官网中Reference/Command-Line Programs
trtexec --onnx=${1} \--memPoolSize=workspace:2048 \--saveEngine=${ENGINE_PATH}/${PREFIX}.engine \--profilingVerbosity=detailed \--dumpOutput \--dumpProfile \--dumpLayerInfo \--exportOutput=${LOG_PATH}/build_output.log \--exportProfile=${LOG_PATH}/build_profile.log \--exportLayerInfo=${LOG_PATH}/build_layer_info.log \--warmUp=200 \--iterations=50 \--verbose \--fp16 \>${LOG_PATH}/build.log
参数说明
- onnx: onnx模型地址,如sample.onnx
- memPoolSize: 生成引擎时,使用memory大小,一般为1024或者2048, 4096
- saveEngine:引擎保存地址
- sparsity=[disable|enable|force]: 是否开启稀疏化结构策略。disable:禁用所有使用结构化稀疏性的策略,默认设置。enable:使用结构化稀疏性启用策略。只有当ONNX文件权重满足结构化稀疏性要求时,才会使用策略。force:启用使用结构化稀疏性的策略,并允许trtexec覆盖ONNX文件中的权重,以强制它们具有结构化稀疏性模式。请注意,精度不会得到保留,这只是为了获得推理性能。
- fp16: 指明精度,同时还支持–bf16 ,–int8, --fp8, ----noTF32,–best。
- minShapes=< shapes>, --optShapes=< shapes>, and --maxShapes=< shapes>: 指明输入的shape
- timingCacheFile:当启用该选项时,TensorRT 在构建新引擎时,会优先从缓存文件中加载历史时序信息,避免对相同层结构重复进行性能分析,显著缩短引擎构建耗时
- verbose:与日志相关
- skipInference: 跳过推理过程
- exportOutput,exportProfile,exportLayerInfo: 输出,分析,层信息相关的输出地址
- precisionConstraints=spec:是否对精度进行约束,因为可能会出现指定了fp16, 但是硬件优化发现fp32强于fp16。
- layerPrecisions=spec: 指定层的量化精度
- layerOutputTypes=spec:指定每一个层的输出类型,fp32/fp16等
- layerDeviceTypes=spec: 指定每一个层的设备类型, DLA/GPU, DLA是边缘设备才有的
- useDLACore:为特定层指定DLA core
- allowGPUFallback:不支持在DLA上执行,退回到GPU上执行
- dynamicPlugins:加载插件动态库,序列化到engine中。
- builderOptimizationLevel:构建引擎的优化等级,等级越高,优化时间越长,模型越好。
- warmup:预热, 200指20ms
2. infer脚本
trtexec --loadEngine=${ENGINE_PATH}/${PREFIX}.engine \--dumpOutput \--dumpProfile \--dumpLayerInfo \--exportOutput=${LOG_PATH}/infer_output.log\--exportProfile=${LOG_PATH}/infer_profile.log \--exportLayerInfo=${LOG_PATH}/infer_layer_info.log \--warmUp=200 \--iterations=50 \> ${LOG_PATH}/infer.log
3. profile脚本
Nsight System是Nvidia官方提供的分析软件,具体安装步骤可以参考下面链接,下一章专门讲解安装和工具使用。
nsys profile \--output=${LOG_PATH}/${PREFIX} \--force-overwrite true \trtexec --loadEngine=${ENGINE_PATH}/${PREFIX}.engine \--warmUp=0 \--duration=0 \--iterations=20 \--noDataTransfers \> ${LOG_PATH}/profile.log
profile文件夹下会生成sample-cbr.nsys-rep
,将此文件用Nsight System软件进行打开。