在上一篇文章中,已经对什么是TensorRT,使用TensorRT进行深度学习模型部署推理的完整流程进行了初步的介绍。本文将详细介绍TensorRT引擎的构建,包括tf2onnx的使用、trtexec命令行的使用和重要参数介绍、如何使用Python API进行TensorRT引擎构建等
Tensorflow模型转换为ONNX 使用以下命令安装tf2onnx
1 pip install tf2onnx -i https://pypi.tuna.tsinghua.edu.cn/simple
tf2onnx是以Python命令行模块的形式使用的,可在终端中输入以下内容,查看tf2onnx的帮助信息
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 python -m tf2onnx -h optional arguments: -h, --help show this help message and exit --input INPUT input from graphdef --graphdef GRAPHDEF input from graphdef --saved-model SAVED_MODEL input from saved model --tag TAG tag to use for saved_model --signature_def SIGNATURE_DEF signature_def from saved_model to use --concrete_function CONCRETE_FUNCTION For TF2.x saved_model, index of func signature in __call__ (--signature_def is ignored) --checkpoint CHECKPOINT input from checkpoint --keras KERAS input from keras model --tflite TFLITE input from tflite model --tfjs TFJS input from tfjs model --large_model use the large model format (for models > 2GB) --output OUTPUT output model file --inputs INPUTS model input_names (optional for saved_model, keras, and tflite) --outputs OUTPUTS model output_names (optional for saved_model, keras, and tflite) --ignore_default IGNORE_DEFAULT comma-separated list of names of PlaceholderWithDefault ops to change into Placeholder ops --use_default USE_DEFAULT comma-separated list of names of PlaceholderWithDefault ops to change into Identity ops using their default value --rename-inputs RENAME_INPUTS input names to use in final model (optional) --rename-outputs RENAME_OUTPUTS output names to use in final model (optional) --use-graph-names (saved model only) skip renaming io using signature names --opset OPSET opset version to use for onnx domain --dequantize remove quantization from model. Only supported for tflite currently. --custom-ops CUSTOM_OPS comma-separated map of custom ops to domains in format OpName:domain. Domain 'ai.onnx.converters.tensorflow' is used by default. --extra_opset EXTRA_OPSET extra opset with format like domain:version, e.g. com.microsoft:1 --load_op_libraries LOAD_OP_LIBRARIES comma-separated list of tf op library paths to register before loading model --target {rs4,rs5,rs6,caffe2,tensorrt,nhwc} target platform --continue_on_error continue_on_error --verbose, -v verbose output, option is additive --debug debug mode --output_frozen_graph OUTPUT_FROZEN_GRAPH output frozen tf graph to file --inputs-as-nchw INPUTS_AS_NCHW transpose inputs as from nhwc to nchw --outputs-as-nchw OUTPUTS_AS_NCHW transpose outputs as from nhwc to nchw Usage Examples: python -m tf2onnx.convert --saved-model saved_model_dir --output model.onnx python -m tf2onnx.convert --input frozen_graph.pb --inputs X:0 --outputs output:0 --output model.onnx python -m tf2onnx.convert --checkpoint checkpoint.meta --inputs X:0 --outputs output:0 --output model.onnx For help and additional information see: https://github.com/onnx/tensorflow-onnx
tf2onnx可以接收很多种类型的深度学习模型格式,例如Tensorflow的.pb、checkpoint、.h5、tflite、tfjs等,也可以接收keras模型,在本实验中,我最开始使用.h5和checkpoint并未成功,原因未找到,.pb是可以正确转换的,后续没有再对该问题进行研究,一直使用.pb格式
将Tensorflow导出的model.pb模型转换为ONNX模型model.onnx的命令如下
1 2 python -m tf2onnx.convert --saved-model model.pb --output model.onnx \ --inputs-as-nchw input
使用--inputs-as-nchw input
的原因是Tensorflow与ONNX使用的内存排布方式不同,Tensorflow中使用的是NHWC,而ONNX使用的是NCHW,因此添加该转换参数,将输入的内存排布修改未NCHW
tf2onnx的--opset
可以手动指定opset version,因为ONNX和TensorRT的版本存在一些对应关系,如果ONNX模型转换TensorRT过程中存在问题,可能是opset version的问题,有可能可以通过手动指定opset version解决。本实验不存在这种问题,因此未手动指定opset,使用默认opset进行转换
Nvidia为适配ONNX推出了一个ONNX GraphSurgeon ](https://docs.nvidia.com/deeplearning/tensorrt/onnx-graphsurgeon/docs/index.html))工具,该工具可以对ONNX的模型计算图进行一些节点修改,可以通过该工具解决一些算子不匹配问题或者进行模型计算图优化。本实验未使用ONNX GraphSurgeon对模型进行修改,这部分的详细内容本专栏不涉及
BatchSize 这里的BatchSize指的是模型推理过程输入数据的BatchSize。TensorRT既支持固定的BatchSize,也支持动态BatchSize(Dynamic Shape),固定的BatchSize能够让TensorRT在构建期更好的优化模型。Dynamic Shape能够让开发者在构建期不指定InputShape,而在运行期再通过API来指定。本实验由于是在JestonNano上进行YOLOv5模型推理,JestonNano本身性能算力有限,因此本实验中在ONNX模型中手动固定了BatchSize为1,以下为使用onnx修改BatchSize的代码
1 2 3 4 5 6 7 8 9 10 11 import onnxonnx_model = onnx.load_model('model.onnx' ) batch_size = 1 inputs = onnx_model.graph.input for input in inputs: dim1 = input .type .tensor_type.shape.dim[0 ] dim1.dim_value = batch_size model_name = 'model-bs1.onnx' onnx.save_model(onnx_model, model_name)
ONNX模型转换为TensorRT引擎 ONNX模型转换为TensorRT引擎有两种方式
trtexec命令行工具
TensorRT API
我比较推荐使用trtexec命令行工具来进行转换,该工具不仅可以进行推理引擎的转换,还可以进行转换后的模型推理性能测试,而且转换过程只需要选取正确的参数即可,不需要额外编写代码
使用trtexec转换模型为TensorRT引擎 默认的trtexec命令并没有添加到环境变量,而是位于/usr/src/tensorrt/bin目录下,将model-bs1.onnx转换为TensorRT引擎的命令如下
1 /usr/src/tensorrt/bin/trtexec --onnx=model-bs1.onnx --saveEngine=model.trt --workspace=3200
以上命令执行后,trtexec开始工作,这需要一段时间,trtexec首先会将ONNX格式进行解析,并进行适用于在GPU上推理的计算图优化过程,然后是各层具体实现算子及参数的自动调优,整个过程都是自动进行的,用户所需要做的仅仅是输入以上命令,然后等待即可
有几点需要说明,首先是trtexec并不仅仅可以接收ONNX格式,也可以接收一些其他格式的模型,本文仅使用了ONNX格式,因此对于其他格式不在此介绍。--saveEngine
后跟要输出的引擎文件名称,这里输入的”model.trt”只是一个文件名字,可以输入任何合法的名称。--workspace
是为trtexec指定的转换过程可用内存空间,单位是MB,由于JestonNano开发板内存大小为4G,以上命令是按照板子实际可用内存配置的,用户需按照自己的实际情况进行设置,太小的数值可能导致构建过程失败
转换精度 转换过程非常重要的一点是转换精度,不同精度的引擎,引擎大小及内部参数占用内内存大小差异很大,由于TensorRT使用GPU进行推理,整个过程涉及到CPU和GPU之间的内存拷贝,较低精度的模型能够在很大程度上的降低内存拷贝和访存时间,对推理速度的影响很大,代价是牺牲一些精度。默认未指定转换精度的情况下,trtexec生成的推理引擎是FP32的,JestonNano上最低支持的精度是FP16,INT8精度在Nvidia NX等更高端的计算平台上才支持,因此INT8精度转换及后续处理不在本系列讨论范围内,需要注意的是INT8精度转换过程需要进行校准操作,需要提供一组校准数据集(unlabel)以及指定校准模式等操作,详细内容可以在TensorRT官方文档中找到。要将模型转换为FP16精度,可按照以下命令操作
1 /usr/src/tensorrt/bin/trtexec --onnx=model-bs1.onnx --saveEngine=xxx-fp16.trt --fp16 --workspace=3200
只需要增加--fp16
选项即可。需要清楚一点,这里使用--fp16
选项,trtexec转换后的模型,输入层和输出层依然是fp32,只有中间层是fp16,这一点在后续推理章节会详细介绍。如果要让输入层和输出层也变为fp16,需要增加--inputIOFormats
和--outputIOFormats
选项,如下
1 2 /usr/src/tensorrt/bin/trtexec --onnx=model-bs1.onnx --saveEngine=xxx-fp16.trt --explicitBatch --inputIOFormats=fp16:chw --outputIOFormats=fp16:chw --fp16 --workspace=3200
FP16比FP32转换所需要的时间更长
转换输出信息 trtexec转换过程末尾的输出信息非常重要,对引擎的推理性能进行了多轮推理测试,这对实际模型的部署过程性能评估具有指导意义
重点关注Throughput和Latency两个参数,这两个参数其实是互为倒数,例如图中所示,平均Latency为108.75ms,Throughput为9.18516,意味着不考虑前处理和后处理耗时,模型推理的极限帧率大概是9.18fps,实际进行推理时的推理延迟和这里的测试数值基本一致
使用TensorRT API转换模型为TensorRT引擎 使用TensorRT API的转换脚本我这里直接给出,具体内容直接看代码吧,就不详细介绍了
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 import osimport sysimport loggingimport argparseimport numpy as npimport tensorrt as trtimport pycuda.driver as cudaimport pycuda.autoinitlogging.basicConfig(level=logging.INFO) logging.getLogger("Engine" ).setLevel(logging.INFO) logger = logging.getLogger('Engine' ) class EngineCalibrator (trt.IInt8MinMaxCalibrator): def __init__ (self, cache_file ): super ().__init__() self .cache_file = cache_file self .calib_batcher = None self .batch_allocation = None self .batch_generator = None def set_calib_batcher (self, calib_batcher: CalibBatcher ): self .calib_batcher = calib_batcher size = int (np.dtype(self .image_batcher.dtype).itemsize * np.prod(self .image_batcher.shape)) self .batch_allocation = cuda.mem_alloc(size) self .batch_generator = self .image_batcher.get_batch() def get_batch_size (self ): if self .calib_batcher: return self .calib_batcher.batch_size return 1 def get_batch (self, names ): if not self .calib_batcher: return None try : batch, _, _ = next (self .batch_generator) logger.info("Calibrating image {} / {}" .format (self .calib_batcher.image_index, self .calib_batcher.num_images)) cuda.memcpy_htod(self .batch_allocation, np.ascontiguousarray(batch)) return [int (self .batch_allocation)] except StopIteration: logger.info("Finished calibration batches" ) return None def read_calibration_cache (self ): if os.path.exists(self .cache_file): with open (self .cache_file, "rb" ) as f: logger.info("Using calibration cache file: {}" .format (self .cache_file)) return f.read() def write_calibration_cache (self, cache ): if self .cache_file is None : return with open (self .cache_file, "wb" ) as f: logger.info("Writing calibration cache data to: {}" .format (self .cache_file)) f.write(cache) def main (args ): logger = trt.Logger(trt.Logger.WARNING) trt.init_libnvinfer_plugins(logger, namespace="" ) builder = trt.Builder(logger) network = builder.create_network(1 << int (trt.NetworkDefinitionCreateFlag.EXPLICIT_BATCH)) parser = trt.OnnxParser(network, logger) success = parser.parse_from_file(args.onnx) for idx in range (parser.num_errors): print (parser.get_error(idx)) if not success: return inputs = [network.get_input(i) for i in range (network.num_inputs)] outputs = [network.get_output(i) for i in range (network.num_outputs)] logger.info('Network Description' ) for input in inputs: logger.info("Input '{}' with shape {} and dtype {}" .format (input .name, input .shape, input .dtype)) for output in outputs: logger.info("Output '{}' with shape {} and dtype {}" .format (output.name, output.shape, output.dtype)) config = builder.create_builder_config() memsize = args.mem * 1024 * 1024 config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, memsize) if args.precision == 'fp16' : if not builder.platform_has_fast_fp16: logger.warning("FP16 is not supported natively on this platform/device" ) config.set_flag(trt.BuilderFlag.FP16) elif args.precision == 'int8' : if not builder.platform_has_fast_int8: logger.warning("INT8 is not supported natively on this platform/device" ) config.set_flag(trt.BuilderFlag.INT8) config.int8_calibrator = EngineCalibrator(args.calib_cache) if args.calib_cache is None or not os.path.exists(args.calib_cache): calib_shape = [args.calib_batch_size] + list (inputs[0 ].shape[1 :]) calib_dtype = trt.nptype(inputs[0 ].dtype) config.int8_calibrator.set_image_batcher( CalibBatcher(args.calib_path, calib_shape, calib_dtype, args.calib_num, exact_batch=True ) ) serialized_engine = builder.builder_serialized_network(network, config) with open (args.output, 'wb' ) as f: logger.info("Serializing engine to file: {:}" .format (args.engine)) f.write(serialized_engine) def parse_args (): parser = argparse.ArgumentParser() parser.add_argument('--onnx' , required=True , help ='input onnx file' ) parser.add_argument('--engine' , default='engine.trt' , help ='output engine filename' ) parser.add_argument('--precision' , default='fp32' , choices=['fp32' , 'fp16' , 'int8' ], help ='the precision of mode, fp32/fp16/int8' ) parser.add_argument('--mem' , default=32 , type =int , help ='memory pool limit(MiB)' ) parser.add_argument('--calib_path' , help ='the int8 calibration images directory' ) parser.add_argument('--calib_cache' , default='./calibration.cache' , help ='the int8 calibration cache save path' ) parser.add_argument('--calib_num' , default=5000 , type =int , help ='the int calibration images number, default 5000' ) parser.add_argument('--calib_batch_size' , default=8 , type =int , help ='the int calibration batch size, default 8' ) return parser.parse_args() if __name__ == '__main__' : args = parse_args() if args.precision == 'int8' and not args.calib_path: logger.error("int8 --calib_path is required" ) sys.exit(1 ) try : main(args) except SystemError: pass