【理论知识】实际部署中tensorrt的简单理解

因为各种项目的原因,其实已经用过一段时间tensorrt了,但是一直没仔细梳理过理论知识了,在b站刷到官方教程,写个博客梳理一下tensorrt基础知识和开发流程。

(写到一半莫名其妙后半部分被csdn吞了,草稿找不到了,以后还是本地写了)

  1. tensorrt简介

tensorrt是NV官方的深度学习部署工具

➢ 用于高效实现已训练好的深度学习模型的推理过程的 SDK ➢ 内含推理优化器和运行时环境 ➢ 使 DL 模型能以更高吞吐量和更低的延迟运行 ➢ 有 C++ 和 python 的 API,完全等价可以混用

tensorrt主要进行以下工作

➢ 构建期(推理优化器) ➢ 模型解析 / 建立 加载 Onnx 等其他格式的模型 / 使用原生 API 搭建模型 ➢ 计算图优化 横向层融合(Conv),纵向层融合(Conv+add+ReLU), …… ➢ 节点消除 去除无用层,节点变换(Pad, Slice, Concat, Shuffle), …… ➢ 多精度支持 FP32 / FP16 / INT8 / TF32(可能插入 reformat 节点) ➢ 优选 kernel / format 硬件有关优化 ➢ 导入 plugin 实现自定义操作 ➢ 显存优化 显存池复用 ➢ 运行期(运行时环境) ➢ 运行时环境 对象生命期管理,内存显存管理,异常处理 ➢ 序列化反序列化 推理引擎保存为文件或从文件中加载

2.tensorrt的部署简要流程

【理论知识】实际部署中tensorrt的简单理解_第1张图片【理论知识】实际部署中tensorrt的简单理解_第2张图片

  1. 搭建tensorrt的基本流程

    ➢ 基本流程 ➢ 构建期 ➢ 建立 Builder(引擎构建器) ➢ 创建 Network(计算图内容) ➢ 生成 SerializedNetwork(网络的 TRT 内部表示) ➢ 运行期 ➢ 建立 Engine 和 Context ➢ Buffer 相关准备(Host 端 + Device 端 + 拷贝操作) ➢ 执行推理(Execute)

    示例代码在官方github的01-SimpleDemo/TensorRT8中的 TRT8-cudart.py 或 TRT8.cpp(python 和 C++ 等价版本)

    官方github上的代码更新过,和教程不完全相同,但思路完全一样。

    构建期:

    logger = trt.Logger(trt.Logger.ERROR)                                       # 指定 Logger,可用等级:VERBOSE,INFO,WARNING,ERRROR,INTERNAL_ERROR
    if os.path.isfile(trtFile):                                                 # 如果有 .plan 文件则直接读取
        with open(trtFile, 'rb') as f:
            engineString = f.read()
        if engineString == None:
            print("Failed getting serialized engine!")
            return
        print("Succeeded getting serialized engine!")
    else:                                                                       # 没有 .plan 文件,从头开始创建
        builder = trt.Builder(logger)                                           # 网络元信息,Builder/Network/BuilderConfig/Profile 相关
        network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
        profile = builder.create_optimization_profile()
        config = builder.create_builder_config()
        config.max_workspace_size = 1 << 30
    ​
        inputTensor = network.add_input('inputT0', trt.DataType.FLOAT, [-1, -1, -1])  # 指定输入张量
        profile.set_shape(inputTensor.name, [1, 1, 1], [3, 4, 5], [6, 8, 10])   # 指定输入张量 Dynamic Shape 范围
        config.add_optimization_profile(profile)
    ​
        identityLayer = network.add_identity(inputTensor)                       # 恒等变换
        network.mark_output(identityLayer.get_output(0))                        # 标记输出张量
    ​
        engineString = builder.build_serialized_network(network, config)        # 生成序列化网络
        if engineString == None:
            print("Failed getting serialized engine!")
            return
        print("Succeeded getting serialized engine!")
        with open(trtFile, 'wb') as f:                                          # 将序列化网络保存为 .plan 文件
            f.write(engineString)
            print("Succeeded saving .plan file!")

    运行期:

    ```engine = trt.Runtime(logger).deserialize_cuda_engine(engineString)          # 使用 Runtime 来创建 engine
    if engine == None:
        print("Failed building engine!")
        return
    print("Succeeded building engine!")
    ​
    context = engine.create_execution_context()                                 # 创建 context(相当于 GPU 进程)
    context.set_binding_shape(0, [3, 4, 5])                                     # Dynamic Shape 模式需要绑定真实数据形状
    nInput = np.sum([engine.binding_is_input(i) for i in range(engine.num_bindings)])  # 获取 engine 绑定信息
    nOutput = engine.num_bindings - nInput
    for i in range(nInput):
        print("Bind[%2d]:i[%2d]->" % (i, i), engine.get_binding_dtype(i), engine.get_binding_shape(i), context.get_binding_shape(i), engine.get_binding_name(i))
    for i in range(nInput,nInput+nOutput):    
        print("Bind[%2d]:o[%2d]->" % (i, i - nInput), engine.get_binding_dtype(i), engine.get_binding_shape(i), context.get_binding_shape(i), engine.get_binding_name(i))
    ​
    data = np.arange(3 * 4 * 5, dtype=np.float32).reshape(3, 4, 5)              # 准备数据和 Host/Device 端内存
    bufferH = []
    bufferH.append(np.ascontiguousarray(data.reshape(-1)))
    for i in range(nInput, nInput + nOutput):
        bufferH.append(np.empty(context.get_binding_shape(i), dtype=trt.nptype(engine.get_binding_dtype(i))))
    bufferD = []
    for i in range(nInput + nOutput):
        bufferD.append(cudart.cudaMalloc(bufferH[i].nbytes)[1])
    ​
    for i in range(nInput):                                                     # 首先将 Host 数据拷贝到 Device 端
        cudart.cudaMemcpy(bufferD[i], bufferH[i].ctypes.data, bufferH[i].nbytes, cudart.cudaMemcpyKind.cudaMemcpyHostToDevice)
    ​
    context.execute_v2(bufferD)                                                 # 运行推理计算
    ​
    for i in range(nInput, nInput + nOutput):                                   # 将结果从 Device 端拷回 Host 端
        cudart.cudaMemcpy(bufferH[i].ctypes.data, bufferD[i], bufferH[i].nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost)
    ​
    for i in range(nInput + nOutput):
        print(engine.get_binding_name(i))
        print(bufferH[i].reshape(context.get_binding_shape(i)))
    ​
    for b in bufferD:                                                           # 释放 Device 端内存
        cudart.cudaFree(b)
  2. tensorrt搭建与开发方式 官方列出了三种方案

    ➢ 使用框架自带 TRT 接口(TF-TRT, Torch-TensorRT) ➢ 简单灵活,部署仍在原框架中,无需书写 Plugin

    ➢ 使用 Parser(TF/Torch/… → ONNX[1] → TensorRT) ➢ 流程成熟, ONNX 通用性好,方便网络调整,兼顾效率性能

    ➢ 使用 Parser(TF/Torch/… → ONNX[1] → TensorRT) ➢ 流程成熟, ONNX 通用性好,方便网络调整,兼顾效率性能

    三种方案各有优劣
    【理论知识】实际部署中tensorrt的简单理解_第3张图片

     

  3. 我自己做过的以及网上开源的项目中,更过是采用第二种方式,第一种方案也有,第三种则见得少一些(毕竟我也是刚开始接触tensorrt)

    github中也给出了使用API搭建MNIST手写识别模型的示例

    不过pytorch和paddle的demo都是TODO待更新,目前只有TensorFlow的

    代码基本流程如下: ➢ TensorFlow 中创建并训练一个网络 ➢ 提取网络权重,保存为 para.npz ➢ TensorRT 中重建该网络并加载 para.npz 中的权重 ➢ 生成推理引擎 ➢ 用引擎做实际推理

  4. tensorrt运行期

    engine是trt中的计算引擎,有点类似于cpu计算中的线程,算是基本计算单元。

    ➢生成 TRT 内部表示 ➢ serializedNetwork = builder. build_serialized_network(network, config) ➢生成 Engine ➢ engine = trt.Runtime(logger).deserialize_cuda_engine( serializedNetwork ) ➢创建 Context ➢ context = engine.create_execution_context() ➢绑定输入输出(Dynamic Shape 模式必须) ➢ context.set_binding_shape(0,[1,1,28,28]) ➢准备 Buffer ➢ inputHost = np.ascontiguousarray(inputData.reshape(-1)) ➢ outputHost = np.empty(context.get_binding_shape(1), trt.nptype(engine.get_binding_dtype(1))) ➢ inputDevice = cudart.cudaMalloc(inputHost.nbytes)[1] ➢ outputDevice = cudart.cudaMalloc(outputHost.nbytes)[1] ➢执行计算 ➢ cudart.cudaMemcpy(inputDevice, inputHost.ctypes.data, inputHost.nbytes, cudart.cudaMemcpyKind.cudaMemcpyHostToDevice) ➢ context.execute_v2([int(inputDevice), int(outputDevice)]) ➢ cudart.cudaMemcpy(outputHost.ctypes.data, outputDevice, outputHost.nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost)

    这里补了一点 CUDA异构计算的知识

    ➢CUDA 异构计算 ➢ 同时准备 CPU 端内存和 GPU端显存 ➢ 开始计算前把数据从内存拷贝到显存中 ➢ 计算过程的输入输出数据均在 GPU端读写 ➢ 计算完成后要把结果拷贝会内存才能使用

    在运行过程中就需要涉及到内存和显存的数据拷贝

    ➢Buffer ➢ 内存和显存的申请 ➢ inputHost = np.ascontiguousarray(inputData.reshape(-1)) ➢ outputHost = np.empty(context.get_binding_shape(1), trt.nptype(engine.get_binding_dtype(1))) ➢ inputDevice = cudart.cudaMalloc(inputHost.nbytes)[1] ➢ outputDevice = cudart.cudaMalloc(outputHost.nbytes)[1]

    ➢ 内存和显存之间的拷贝 ➢ cudart.cudaMemcpy(inputDevice, inputHost.ctypes.data, inputHost.nbytes, cudart.cudaMemcpyKind.cudaMemcpyHostToDevice) ➢ cudart.cudaMemcpy(outputHost.ctypes.data, outputDevice, outputHost.nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost)

    ➢ 推理完成后释放显存 ➢ cudart.cudaFree(inputDevice) ➢ cudart.cudaFree(outputDevice)

  5. 1

  6. 1

  7. 开发者辅助工具

    ➢trtexec TensorRT 命令行工具,主要的 End2End 性能测试工具 ➢Netron 网络可视化 ➢onnx-graphsurgeon onnx 计算图编辑 ➢polygraphy 结果验证与定位,图优化 ➢Nsight Systems 性能分析

    等用熟了之后看看能不能单独写个博客

主要参考自nvidia官方给出的教程,教程是基于8.2.3版本的TensorRT。

trt-samples-for-hackathon-cn/cookbook at master · NVIDIA/trt-samples-for-hackathon-cn · GitHub

在b站官方还有个简单的讲解视频,不过个人觉得ppt比讲解本身重要一点。

以及

(7条消息) TensorRT 之入门篇点PY的博客-CSDN博客tensorrt教程

你可能感兴趣的:(深度学习实际部署,深度学习,计算机视觉,pytorch)