最近一段时间接触了TensorRT,因此顺带整理了一份TensorRT中一些常用关键组件的笔记,以方便未来能够快速查阅和上手。需要注意的是本文只是一些常用特性与关键组件的梳理,而不是一个小白入门的教程,所以后续文章内容并不会提供快速上手的样例代码,实际上TensorRT官方的example中已经挺完善了,从0到1入门的新手建议直接看官方example中的代码。
版本说明:本文主要以 TensorRT 8.0 为例说明
当前 PyTorch 模型要部署到 TenosrRT 上需要通过 ONNX 做桥接,因此一个 PyTorch 模型需要先转换成 ONNX,然后再由 ONNX 转换成 TensorRT engine 对象,才能实现在 TensorRT 的目标硬件上部署。
TensorRT 上主要存在以下几个对象:
TensorRT 提供了多种渠道来构建 Network 对象:
1.使用 TensorRT 内置接口搭建模型结构,然后将其它框架的参数映射过去。
2.使用对应的 Parser 将目标模型解析然后将结果自动填充到 TensorRT network 中,当前 TensorRT 支持 ONNX、Caffe 和 UFF 三种类型 Parser,因此 PyTorch 模型可以通过 ONNX 实现与 TensorRT 的对接部署。
使用 parser 对象可以从目标模型文件中解析出模型结构与模型参数,然后自动填充到 network 中,parser 的使用方式如下。
EXPLICIT_BATCH = 1 << (int)(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
with trt.Builder(TRT_LOGGER) as builder, builder.create_network(EXPLICIT_BATCH) as network, trt.OnnxParser(network, TRT_LOGGER) as parser:
with open(model_path, 'rb') as model:
if not parser.parse(model.read()):
for error in range(parser.num_errors):
print(parser.get_error(error))
TensorRT 目前支持的 Parser 有:
1.UffParser
2.CaffeParser
3.OnnxParser
当 network 与 config 都准备就绪后,需要通过 builder 来构建 engine,builder 构建 engine 的其中一步就是从众多的 CUDA kernel 中搜索出一组当前条件下的最优配置,以此来保证 inference 的高效执行。
engine = builder.build_engine(network, config)
1.解析 network graph,注册计算层
2.删除冗余的常量节点与无用层
3.进行 model fusion,构成新的计算层
4.计算层最优执行 kernel 搜索
5.打包最终的 kernel 方案构成 engine
为了避免每次运行 inference 之前都需要对 engine 进行编译,用户可以将 engine 序列化后持久化到本地。
# 序列化
serialized_engine = engine.serialize()
# 反序列化
with trt.Runtime(TRT_LOGGER) as runtime:
engine = runtime.deserialize_cuda_engine(serialized_engine)
# 持久化序列化后的 engine
with open(“sample.engine”, “wb”) as f:
f.write(engine.serialize())
# 加载持久化的 engine,并反序列化
with open(“sample.engine”, “rb”) as f, trt.Runtime(TRT_LOGGER) as runtime:
engine = runtime.deserialize_cuda_engine(f.read())
因为 TensorRT 是基于 CUDA 构建的,在编程模型上也集成了 CUDA 的抽象,将设备抽象为 host 与 device,并且用户需要显示地把数据从 host 拷贝到 device 中,反之亦然,device 输出的数据需要显示地拷贝到 host 中。
因此在开始 inference 之前,需要先在 host 与 device 的 memory 中申请 buffer,同时 host 中的 memory 将被设定成 page-lock 类型,该类型可以保证该 buffer 所对应的内存页不会被操作系统换出到磁盘。
假设当前模型是单输入与单输出的,那么对应的 engine 将只有两个 binding,可以通过 for binding in engine 的方式来遍历获取 binding,然后对每个 binding 对应输入输出分别去申请 memory buffer。
# Simple helper data class that's a little nicer to use than a 2-tuple.
class HostDeviceMem(object):
def __init__(self, host_mem, device_mem):
self.host = host_mem
self.device = device_mem
def __str__(self):
return "Host:\n" + str(self.host) + "\nDevice:\n" + str(self.device)
def __repr__(self):
return self.__str__()
# Allocates all buffers required for an engine, i.e. host/device inputs/outputs.
def allocate_buffers(engine):
inputs = []
outputs = []
bindings = []
stream = cuda.Stream()
for binding in engine:
size = trt.volume(engine.get_binding_shape(binding)) * engine.max_batch_size
dtype = trt.nptype(engine.get_binding_dtype(binding))
# Allocate host and device buffers
# 申请锁页内存
host_mem = cuda.pagelocked_empty(size, dtype)
device_mem = cuda.mem_alloc(host_mem.nbytes)
# Append the device buffer to device bindings.
bindings.append(int(device_mem))
# Append to the appropriate list.
if engine.binding_is_input(binding):
inputs.append(HostDeviceMem(host_mem, device_mem))
else:
outputs.append(HostDeviceMem(host_mem, device_mem))
return inputs, outputs, bindings, stream
后续在 inference 过程中,将输入数据放入 inputs buffer,从 outputs buffer 中取出计算结果。
context 是由 engine 创建出来的,一个 engine 可以创建多个 context,且 context 需要与 cuda stream 进行绑定。通过 engine 创建 context。
context = engine.create_execution_context()
使用 context 进行 inference,这个过程涉及数据搬运与 inference 执行,输入与返回数据均要求是 numpy array。
# This function is generalized for multiple inputs/outputs.
# inputs and outputs are expected to be lists of HostDeviceMem objects.
def do_inference(context, bindings, inputs, outputs, stream, batch_size=1):
# Transfer input data to the GPU.
[cuda.memcpy_htod_async(inp.device, inp.host, stream) for inp in inputs]
# Run inference.
context.execute_async(batch_size=batch_size, bindings=bindings, stream_handle=stream.handle)
# Transfer predictions back from the GPU.
[cuda.memcpy_dtoh_async(out.host, out.device, stream) for out in outputs]
# Synchronize the stream
stream.synchronize()
# Return only the host outputs.
return [out.host for out in outputs]
Q 指的是 quantize layer,DQ 指的是 dequantize layer
TensorRT 的设计中,但凡是需要使用量化计算的层,该层的前后就需要成对出现 quantize 与 dequantize 层。
在 TensorRT 中,一个 Quantizable-layer 指的是整型输入经过 DQ 层,然后经过目标层计算后,输出再经 Q 层进行量化,这个目标层被称为 Quantizable-layer,比如 avg_pool。以下图为例在实际部署中,DQ 与目标层与 Q 层将被融合,而融合后的层被称为 quantized AvgPool。
根据官方文档,TensorRT 目前只支持 symmetric uniform 形式的量化参数映射,也就是说 zero_point 恒为 0。
TensorRT 的 PTQ 模式下,通常得到的量化校准后的模型都是一个混合精度的量化模型。因为根据官方文档描述 PTQ 模式下,TensorRT 的设计是以计算性能作为第一优先指标,它首先基于原始的浮点精度模型进行图优化操作,然后选择其中的一些层进行 INT8 量化尝试,如果这些层在 INT8 下的计算性能比 FP16 或者 FP32 来得更高,那么这些层将采用 INT8 计算,但是如果性能并没有更高,就会采用 FP32 或者 FP16 方式计算。因此经过 TensorRT PTQ 校准之后得到的 engine 是一个在目标硬件上性能最优的混合精度的量化模型,并且其中哪些层会采用 INT8 量化计算也非用户可以控制。
PTQ 支持四种校验模式:
关于校验数据: 通常校验数据会从训练集中取一个子集出来,子集的数量也并非一定越大越好。就 Imagenet-1k 的分类经验而言,校验数据取 1000 张样本就可以取得不错的效果,反而如果使用了更多的样本数据,效果反而可能下降。
TensorRT PTQ 模式支持两种方式取得量化参数:
1.用户指定用于 PTQ 的 calibration data,然后执行 build engine,PTQ 将使用 calibration data 自动去获取量化参数,但是最终出来的模型,哪些层会以 INT8 的格式跑,哪些层会以 FP16/FP32 的格式跑是不可预知、不可控的。
2.用户手动给需要量化的目标层指定 layer.precision、layer.get_input(0).dynamic_range 与 layer.get_output(0).dynamic_range,然后执行 build engine,然后 TensorRT 将直接使用用户提供的参数编译得到含有量化层的 engine。
❗️针对第二点需要注意的是,layer.precision、layer.get_input(0).dynamic_range 与 layer.get_output(0).dynamic_range 这三个参数都需要有,否则即便设置了 builder.strict_type_constraints=True,TensorRT 为了编译顺利,也会将参数缺失的 layer fallback 到 FP16/FP32 去。这点对于需要严格控制量化层的用户来说需要谨慎。
TensorRT 中常见的 fusion 有一下几种:
注意:以上之所以没有提到 bn,是因为默认 conv+bn 已经做了合并。
百分百由用户掌控量化与反量化的位置,但是需要 ONNX 的 Q/DQ 操作配合,也就是用户通过在 ONNX IR 中显示地插入 Q/DQ 层来控制量化与反量化的位置。相关的案例可以参考:pytorch-quantization。
PTQ VS. explicit-quantization 另一点值得注意的是,在将含有 Q/DQ 层的 ONNX 模型导出以后,用于构建 TensorRT engine 的时候,需要将添加以下配置项,这些是在 pytorch-quantization 与 TenosorRT 文档中没有提到的:
import tensorrt as trt
EXPLICIT_BATCH = 1 << (int)(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
# 指定使用 EXPLICIT_PRECISION
EXPLICIT_PRECISION = 1 << (int)(trt.NetworkDefinitionCreationFlag.EXPLICIT_PRECISION)
builder = trt.Builder(TRT_LOGGER)
network = builder.create_network(EXPLICIT_BATCH | EXPLICIT_PRECISION)
config = builder.create_builder_config()
# 开启 builder int8 flag
config.set_flag(trt.BuilderFlag.INT8)
如果没有在 create_network 的时候指定 EXPLICIT_PRECISION,那么 build engine 的时候将碰到以下错误:
ERROR: Failed to parse the ONNX file.
In node -1 (importConv): UNSUPPORTED_NODE: Assertion failed: ctx->network()->hasExplicitPrecision() && "TensorRT only supports multi-input conv for explicit precision QAT networks!"
而如果 config 没有设定 trt.BuilderFlag.INT8 将遇到以下错误:
[TensorRT] ERROR: Int8 precision has been set for a layer or layer output, but int8 is not configured in the builder
[TensorRT] ERROR: Network validation failed.
TensorRT PTQ 接口的实现是朝着性能最优进行优化,所以经过 PTQ 自动优化之后得到的模型是一个混合精度模型,这样在模型 inference 过程中不利于用户理解哪一层究竟采用了哪种数据精度(INT8/FP16/FP32)。具体来说 TensorRT PTQ 优化的时候遵循着以下规则:
不过所幸只要将 TensorRT 的 Logger 设置为 VERBOSE 类型,那么在编译 engine 完成之后,TensorRT 的日志系统会自动输出每一层的所使用的算子类型、数据精度等信息。
以下信息是使用 resnet18 ONNX 模型编译 TensorRT engine 的输出结果,可以看到此次 engine 的编译花费了 10 秒钟。
[TensorRT] VERBOSE: Engine generation completed in 10.7146 seconds.
[TensorRT] VERBOSE: Builder timing cache: created 62 entries, 58 hit(s)
[TensorRT] VERBOSE: Engine Layer Information:
[TensorRT] VERBOSE: Layer(scudnn): Conv_0 + Relu_1, Tactic: -4420849921117327522, input[Float(3,224,224)] -> 125[Float(64,112,112)]
[TensorRT] VERBOSE: Layer(PoolingTiled): MaxPool_2, Tactic: 6947073, 125[Float(64,112,112)] -> 126[Float(64,56,56)]
[TensorRT] VERBOSE: Layer(scudnn_winograd): Conv_3 + Relu_4, Tactic: 2775507031594384867, 126[Float(64,56,56)] -> 129[Float(64,56,56)]
[TensorRT] VERBOSE: Layer(scudnn_winograd): Conv_5 + Add_6 + Relu_7, Tactic: 2775507031594384867, 129[Float(64,56,56)], 126[Float(64,56,56)] -> 133[Float(64,56,56)]
[TensorRT] VERBOSE: Layer(scudnn_winograd): Conv_8 + Relu_9, Tactic: 2775507031594384867, 133[Float(64,56,56)] -> 136[Float(64,56,56)]
[TensorRT] VERBOSE: Layer(scudnn_winograd): Conv_10 + Add_11 + Relu_12, Tactic: 2775507031594384867, 136[Float(64,56,56)], 133[Float(64,56,56)] -> 140[Float(64,56,56)]
[TensorRT] VERBOSE: Layer(Convolution): Conv_13 + Relu_14, Tactic: 1, 140[Float(64,56,56)] -> 143[Float(128,28,28)]
[TensorRT] VERBOSE: Layer(FusedConvActDirect): Conv_15, Tactic: 8847359, 143[Float(128,28,28)] -> 210[Float(128,28,28)]
[TensorRT] VERBOSE: Layer(scudnn): Conv_16 + Add_17 + Relu_18, Tactic: -4420849921117327522, 140[Float(64,56,56)], 210[Float(128,28,28)] -> 149[Float(128,28,28)]
[TensorRT] VERBOSE: Layer(FusedConvActDirect): Conv_19 + Relu_20, Tactic: 8847359, 149[Float(128,28,28)] -> 152[Float(128,28,28)]
[TensorRT] VERBOSE: Layer(scudnn_winograd): Conv_21 + Add_22 + Relu_23, Tactic: 2775507031594384867, 152[Float(128,28,28)], 149[Float(128,28,28)] -> 156[Float(128,28,28)]
[TensorRT] VERBOSE: Layer(Convolution): Conv_24 + Relu_25, Tactic: 1, 156[Float(128,28,28)] -> 159[Float(256,14,14)]
[TensorRT] VERBOSE: Layer(FusedConvActDirect): Conv_26, Tactic: 7274495, 159[Float(256,14,14)] -> 225[Float(256,14,14)]
[TensorRT] VERBOSE: Layer(Convolution): Conv_27 + Add_28 + Relu_29, Tactic: 56, 156[Float(128,28,28)], 225[Float(256,14,14)] -> 165[Float(256,14,14)]
[TensorRT] VERBOSE: Layer(FusedConvActDirect): Conv_30 + Relu_31, Tactic: 2621439, 165[Float(256,14,14)] -> 168[Float(256,14,14)]
[TensorRT] VERBOSE: Layer(scudnn_winograd): Conv_32 + Add_33 + Relu_34, Tactic: 2775507031594384867, 168[Float(256,14,14)], 165[Float(256,14,14)] -> 172[Float(256,14,14)]
[TensorRT] VERBOSE: Layer(Reformat): Conv_35 + Relu_36 input reformatter 0, Tactic: 1002, 172[Float(256,14,14)] -> Conv_35 + Relu_36 reformatted input 0[Float(256,14,14)]
[TensorRT] VERBOSE: Layer(scudnn): Conv_35 + Relu_36, Tactic: 5863767799113001648, Conv_35 + Relu_36 reformatted input 0[Float(256,14,14)] -> 175[Float(512,7,7)]
[TensorRT] VERBOSE: Layer(Reformat): Conv_37 input reformatter 0, Tactic: 0, 175[Float(512,7,7)] -> Conv_37 reformatted input 0[Float(512,7,7)]
[TensorRT] VERBOSE: Layer(FusedConvActDirect): Conv_37, Tactic: 10682367, Conv_37 reformatted input 0[Float(512,7,7)] -> 240[Float(512,7,7)]
[TensorRT] VERBOSE: Layer(Convolution): Conv_38 + Add_39 + Relu_40, Tactic: 56, 172[Float(256,14,14)], 240[Float(512,7,7)] -> 181[Float(512,7,7)]
[TensorRT] VERBOSE: Layer(FusedConvActDirect): Conv_41 + Relu_42, Tactic: 10682367, 181[Float(512,7,7)] -> 184[Float(512,7,7)]
[TensorRT] VERBOSE: Layer(scudnn_winograd): Conv_43 + Add_44 + Relu_45, Tactic: 2775507031594384867, 184[Float(512,7,7)], 181[Float(512,7,7)] -> 188[Float(512,7,7)]
[TensorRT] VERBOSE: Layer(PoolingTiled): GlobalAveragePool_46, Tactic: 8192257, 188[Float(512,7,7)] -> 189[Float(512,1,1)]
[TensorRT] VERBOSE: Layer(conv1x1): Gemm_48, Tactic: -1243792514564525921, 189[Float(512,1,1)] -> (Unnamed Layer* 49) [Fully Connected]_output[Float(1000,1,1)]
其中每一层 Layer(xxxxx) 表示了 TensorRT engine 中的 layer 构成,括号中列出了具体负责执行的算子名字,比如 scudnn、scudnn_winograd 等。而冒号后面接的形如 Conv_0 + Relu_1 的字符串反应了当前 Layer 对应的原始 ONNX 模型中的节点,所以可以看到 TensorRT 进行了算子融合。
这里还有一款用于TensorRT可视化的工具:https://github.com/pytorch/pytorch/pull/66431/files。
reformat 层是用于数据格式转换的,比如上一层输出为 FP32,而下一层输入要求 INT8,那么 TensorRT 就会在这两层之间加入 reformat,用于完成数据转换。
事实上如果只是需要使用 TensorRT GA 部分的功能,还可以通过 pip 指令直接安装。但是如果还想使用 TensorRT 配套的一些其它工具,还是需要从官网下载 TensorRT 压缩包安装。
pip install nvidia-pyindex
pip install nvidia-tensorrt