TensorRT

TensorRT

提高性能方式

  • 算子融合(层与张量融合):简单来说就是通过融合一些计算op或者去掉一些多余op来减少数据流通次数以及显存的频繁使用来提速
  • 量化:量化即IN8量化或者FP16以及TF32等不同于常规FP32精度的使用,这些精度可以显著提升模型执行速度并且不会保持原先模型的精度
  • 内核自动调整:根据不同的显卡构架、SM数量、内核频率等(例如1080TI和2080TI),选择不同的优化策略以及计算方式,寻找最合适当前构架的计算方式
  • 动态张量显存:我们都知道,显存的开辟和释放是比较耗时的,通过调整一些策略可以减少模型中这些操作的次数,从而可以减少模型运行的时间
  • 多流执行:使用CUDA中的stream技术,最大化实现并行操作

高性能

定义:时间/空间复杂度更低,即算法复杂度低,保证计算的同时内存消耗低,耗时短.

TensorRT密集型操作更友好,使用TensorRT实现高性能深度学习推理

要点:

  1. 尽量使得GPU高密集度运行,避免出现CPU、GPU相互交换运行(非常耗时)
  2. 尽可能使tensorRT运行多个batch 数据。采用多线程。
  3. 预处理尽量cuda化,例如图像需要做normalize、reisze、warpaffine、bgr2rgb等,在这里,采用cuda核实现warpaffine+normalize等操作,集中在一起性能好
  4. 后处理尽量cuda化,例如decode、nms等。在这里用cuda核实现了decode和nms
  5. 善于使用cudaStream,将操作加入流中,采用异步操作避免等待
  6. 内存复用

硬件编解码

imread

  • NvJPEG硬件解码图像
  • NVDEC硬件解码视频

建立模型流程model_build

  1. 继承自nvinfer1::ILogger定义logger类

  2. 实例化logger,作为全局日志打印的东西

  3. 构建模型编译器

  • 创建网络 创建编译配置

    auto builder = nvinfer1::createInferBuilder(logger);
    auto network = builder->createNetworkV2(1);//其中1表示显示批处理
    auto config  = builder->createBuilderConfig();
    
  • 配置网络参数, 配置最大的batchsize,意味着推理所指定的batch参数不能超过这个

    builder->setMaxBatchSize(1);
    
  • 配置工作空间的大小, 每个节点不用自己管理内存空间,不用自己去cudaMalloc内存, 使得所有节点均把workspace当做内存池,重复使用,使得内存

    config->setMaxWorkspaceSize(1 << 30);
    
  • 默认情况下,使用的是FP32推理,如果希望使用FP16,可以设置这个flags

    builder->platformHasFastFp16(); //这个函数告诉你,当前显卡是否具有fp16加速的能力
    builder->platformHasFastInt8(); //这个函数告诉你,当前显卡是否具有int8的加速能力
    
  • 如果要使用int8,则需要做精度标定,模型量化内容,把你的权重变为int8格式,计算乘法。减少浮点数乘法操作,用整数(int8)来替代

  1. 构建网络结构;

    1. 自定义网络结构,并赋值权重

    2. 采用nvidia提供的nvonnxparser

    3. 自行编译nvonnxparser

  2. 使用构建好的网络,编译引擎

    auto engine = builder->buildEngineWithConfig(*network, *config)
    
  3. 序列化模型为数据,并储存为文件

    auto host_memory = engine->serialize();  save_to_file("04.cnn.trtmodel", host_memory->data(), host_memory->size());
    
  4. 回收内存

trtexec

可实现转engine模型

cd /TensorRT-7.2.3.4/bin

模型推理流程inferrence

例:04.tensorRT.cnn.cpp

  1. 设置推理用的设备,创建流
cudaSetDevice(0);
cudaStreamCreate(&stream);
  1. 加载模型数据
auto model_data = load_from_file("04.cnn.trtmodel");
  1. 创建运行时实例对象,并反序列化模型, 通过引擎,创建执行上下文
auto runtime = nvinfer1::createInferRuntime(logger);
auto engine = runtime->deserializeCudaEngine(model_data.data(), model_data.size());
auto context = engine->createExecutionContext();
  1. 获取绑定的tensor信息,并打印出来,所谓绑定的tensor,就是指输入和输出节点
int nbindings = engine->getNbBindings();
  1. 分配输入数据和输出内存空间
// 分配输入的设备空间
float* input_device_image = nullptr;
size_t input_device_image_bytes = sizeof(float) * input_host_image.size();
cudaMalloc(&input_device_image, input_device_image_bytes);
// 创建流,并异步的方式复制输入数据到设备
cudaStream_t stream = nullptr;
cudaStreamCreate(&stream);
cudaMemcpyAsync(input_device_image, input_host_image.data(), input_device_image_bytes, cudaMemcpyHostToDevice, stream);
// 分配输出的设备空间
float* output_device = nullptr;
size_t output_device_bytes = 1000 * sizeof(float);
cudaMalloc(&output_device, output_device_bytes);
  1. 入队并进行异步推理
void* bindings[] = {input_device_image, output_device};
context->enqueueV2(1, bindings, stream, nullptr);
  1. 异步复制结果
vector output_predict(1000);
cudaMemcpyAsync(output_predict.data(), output_device, output_device_bytes, cudaMemcpyDeviceToHost, stream);

enqueue的第四个参数inputConsumed,是通知input_device可以被修改的事件指针
如果在这里cudaEventSynchronize(inputConsumed);,在这句同步以后,input_device就可以被修改干别的事情

  1. 同步流,确保结果执行完成
cudaStreamSynchronize(stream);
  1. 打印最后的结果
  2. 释放内存

细节

  1. 注意推理时的预处理,指定了rgb与bgr对调
  2. 如果需要多个图像推理,需要:
    1. 在编译时,指定maxbatchsize为多个图
    2. 在推理时,指定输入的bindings shape的batch维度为使用的图像数,要求小于等于maxbatchsize
    3. 在收取结果的时候,tensor的shape是input指定的batch大小,按照batch处理即可

ONNX

  1. onnx框架,依赖自protobuf做序列化解析文件, nvonnxparser解析器,libnvonnxparser.so。

  2. 如果解析器与pytorch的onnx版本不匹配时就会导致莫名的错误、 如果解析器与pytorch的protobuf版本不匹配,也会导致错误,无法加载nvonnxparser解析器,

  3. nvidia开源,所以可以直接拿来编译使用。 https://github.com/onnx/onnx-tensorrt

  4. 但是nvonnxparser解析器,自身也有版本问题,他的版本需要配合tensorRT版本一起使用

  5. nvonnxparser这个解析器,不同版本有不同的坑,这个与动态batchsize有关。 比较好的搭配,是https://github.com/onnx/onnx-tensorrt/tree/6.0 版本

ONNX与Protobuf

  1. onnx框架,依赖自protobuf做序列化解析文件。

  2. Protobuf通过onnx-ml.proto编译得到onnx-ml.pb.h和onnx-ml.pb.cc或onnx_ml_pb2.py

  3. onnx-ml.pb.cc的代码操作onnx模型文件,实现增删改

  4. onnx-ml.proto描述onnx文件如何组成,结构

推理发生错误解决方案:

  1. 下载特定的protobuf,这里我用的是3.11.4

  2. 下载onnx,保留其proto协议文件,生成pb.h、pb.cpp,只使用这几个即可,不需要onnx全部,Protocol Buffers,为了解决任何语言之间的数据序列化反序列化工作

    1. 通过proto协议文件,定义数据的结构
    2. 通过protoc程序,对xx.proto进行编译,编译为指定语言的输出,输出结果是代码
      以c++为例,输出是xx.pb.cpp和xx.pb.h
      以python为例,输出是xx_pb2.py
    3. 使用生成的代码,对数据进行编码或者解析
    4. protocal buffers有两个储存格式,一种是文字形式,一种是二进制形式
  3. 下载onnx-tensorrt,配合onnx、protobuf等一起加入到项目进行编译,此时nvonnxparser可以替换为
    项目内的源代码,那么任何错误都可以在源代码中进行调试

ONNX文件操作

查看节点model.graph.node

删除节点model.graph.node.remove(item)先修改输入输出

添加节点:onnx.helper.make_node(name, op_type, inputs, outputs, axes)

onnx拼接:

  1. 先把pre_onnx的所有节点和输入输出加上前缀n.name = f'pre/{n.name}'
  2. 把yolov5s的image的输入节点修改为pre_onnx的输出节点
  3. 把pre_onnx的node全部放到yolov5s的node中
  4. 把pre_onnx的输入名称作为yolov5s的input名称

正确导出ONNX

  1. 对于任何用到shape、size返回值的参数时,例如:tensor.view(tensor.size(0), -1)这类操作,避免直接使用tensor.size的返回值,而是加上int转换,tensor.view(int(tensor.size(0)), -1),断开跟踪

  2. 对于nn.Upsample或nn.functional.interpolate函数,使用scale_factor指定倍率,而不是使用size参数指定大小

  3. 对于reshape、view操作时,-1的指定请放到batch维度。其他维度可以计算出来即可。batch维度禁止指定为大于-1的明确数字

  4. torch.onnx.export指定dynamic_axes参数,并且只指定batch维度,禁止其他动态

  5. 使用opset_version=11,不要低于11

  6. 避免使用inplace操作,例如y[…, 0:2] = y[…, 0:2] * 2 - 0.5

  7. 尽量少的出现5个维度,例如ShuffleNet Module,可以考虑合并wh避免出现5维

  8. 尽量把让后处理部分在onnx模型中实现,降低后处理复杂度

简化过程的复杂度,去掉gather、shape类的节点,很多时候,部分不这么改看似也是可以但是需求复杂后,依旧存在各类问题。按照说的这么修改,基本总能成。做了这些,就不需要使用onnx-simplifer了

Int8量化

利用int8乘法替换float32乘法实现性能加速, 对计算过程提高4倍加速

**量化模式:**1.PTQ训练后量化;2.QAT量化感知训练:TensorRT8.0后版本提供,一般在训练框架中进行

int8标定:

**目的:**使确定编码所用参数是否合适

采用KL散度衡量两个分布之间的差异。

细节:

  1. 标定预处理必须与推理过程预处理一致
  2. getBatchSize,标定的batch是多少
  3. getBatch,标定的输入数据是什么,把指针赋值给bindings即可,返回false表示没有数据了
  4. readCalibrationCache,若从缓存文件加载标定信息,则可避免读取文件和预处理,若该函数返回空指针则表示没有缓存,程序会重新通过getBatch重新计算
  5. writeCalibrationCache,当标定结束后,会调用该函数,我们可以储存标定后的缓存结果,多次标定可以使用该缓存实现加速

int8不会大幅降低精度原因

  1. 将低精度计算造成的损失理解为一种噪声;
  2. weight大部分服从正态分布,值域小且对称

量化算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y6dpur23-1680900703022)(TensorRT.assets/image-20221116145759444-16685818867501.png)]

左边非对称量化,右边对称量化

  1. 动态对称量化:onnx

  2. ​ 动态非对称量化算法

    能够更好的捕捉到权重分布,不友好,用的不多

  3. ​ 静态对称量化算法:TensorRT

量化模式

PTQ训练后量化

Int8量化步骤:

  1. 配置setFlag nvinfer1::BuilderFlag::kINT8

  2. 实现Int8EntropyCalibrator类并继承自IInt8EntropyCalibrator2

  3. 实例化Int8EntropyCalibrator并且设置到config.setInt8Calibrator

  4. Int8EntropyCalibrator的作用,是读取并预处理图像数据作为输入

QAT感知量化训练:

量化感知训练钱需要将BN层融合到conv层中

一般都是在训练框架中去做,比如Pytorch

  1. 在训练的前向和后向传递期间,所有权重和激活都被“假量化”
    浮点值四舍五入以模仿 int8 值,但所有计算仍然使用浮点数完成。
  2. 训练期间的所有权重调整都是在“意识到”模型最终会被量化的情况下进行的;
    在量化之后,这种方法通常会产生比动态量化或训练后静态量化更高的准确度

量化的优点

  1. 加快推理速度,访问一次32位浮点型可以访问4次int8整型,整型运算比浮点型运算更快;
  2. 减少存储空间,在边缘侧存储空间不足时更具有意义;
  3. 减少设备功耗,内存耗用少了推理速度快了自然减少了设备功耗;
  4. 易于在线升级,模型更小意味着更加容易传输;
  5. 减少内存占用,更小的模型大小意味着不再需要更多的内存;

量化的缺点

  1. 模型量化增加了操作复杂度,在量化时需要做一些特殊的处理,否则精度损失更严重;
  2. 模型量化会损失一定的精度,虽然在微调后可以减少精度损失,但推理精度确实下降.

TensorRT插件

**作用:**实现tensorrt不支持的算子。例如HWish

通过官方插件:

编译官方plugin库,将生成的libnvinfer_plugin.so.7替换成原本的.so文件。

编写流程

  1. python实现导出onnx

    1. 对插件的layer,写类A, 继承自torch.autograd.Funtion
    2. 类A中增加@staticmethod的一个静态方法,
    3. g.op导出的插件名称就叫“Plugin”:g.op(“Plugin”,x,p)
    4. name_s = “模型名称”
    5. info_s = json.dumps(XXX) 会通过json解析,反序列化
  2. 继承自TRTPlugin

    1. new_config用于返回自定义config类并进行配置
    2. getOutputDimensions返回layer处理后的tensor大小
    3. enqueue实现具体推理工作
  3. SetupPlugin()

  4. 主要实现enqueue

  5. RegisterPlugin(): onnxplugin.hpp中,

知识点

  1. 对插件进行了封装,使得用起来更简单
  2. 在onnx-tensorrt中添加了onnxplugin.cpp,实现对IPluginV2DynamicExt的封装
  3. 在onnx-tensorrt/builtin_op_importers.cpp:5095行,添加了Plugin的解析支持
    • DEFINE_BUILTIN_OP_IMPORTER(Plugin)
    • 使得只要名字是Plugin的节点,都可以解释到该函数上
    • 在代码中,为通用插件提供了支持,使得使用者只需要继承简单的插件接口即可完成需求
  4. 在gen-onnx.py导出时,symbolic函数返回时,g.op返回的永远都是Plugin这个名字,然后name_s指定为自己注册的插件名称,info_s则传递为json字符串,那么复合属性就可以轻易得到支持

封装后的插件实现

  1. 导出onnx时,按照gen-onnx.py,在symbolic函数返回时,指定g.op的name为Plugin
  2. 指定g.op中name_s属性为注册的插件名称,对应后续插件类的类名
  3. 指定g.op中info_属性为需要读取的复合属性,字符串。通常可以传递json,使得属性再复杂都可以,避免使用官方的方式
  4. 创建easy-plugin.cu文件,定义自己的类并继承自ONNXPlugin::TRTPlugin
  5. 实现需要的函数
    • config_finish[非必要]:配置完成函数
      • 当插件配置完毕时调用,可以在其中拿到各种属性,例如info、weights等
    • new_config[非必要]:实例化一个配置对象
      • 可以自定义LayerConfig类并返回,也可以直接使用LayerConfig类
      • 这个函数最大的作用,是配置本插件支持的数据格式和类型。比如fp32和fp16的支持等
    • getOutputDimensions[非必要],获取该插件输出的shape大小,默认取第一个输入的大小
      • 对应于原始插件的getOutputDimensions函数
    • enqueue[必要],插件推理过程
      • 插件的实际推理过程,该函数可能在编译和推理阶段数次调用
  6. 注册插件,使用RegisterPlugin宏
    • RegisterPlugin(MYSELU);
    • 格式是RegisterPlugin(类名);
  7. end

封装

目的:降低tensorrt使用门槛和集成难度,避免重复代码,关注业务逻辑,而非复杂细节。

RAII:

资源获取即初始化

目的:创建资源时初始化,哪里分配哪里释放,再配合接口模式

原则:

  1. 头文件尽量只包含需要的部分
  2. hpp中不写using namespace

Tensor类封装:

**目的:**实现内存管理,维度管理,偏移量计算,索引计算,CPU/GPU互相自动拷贝。

内存拷贝:

  1. 定义内存状态,表示当前最新的内存所在位置(GPU,CPU,Init)
  2. 懒分配原则,需要时才分配
  3. 获取内存地址:tensor.cpu表示拿到最新数据放到cpu

Builder封装

目的:实现onnx到engine的转换封装,int8封装,

  1. 模型编译接口
  2. Int8 标定数据处理
  3. 插件处理,自定义插件支持
  4. 特殊处理,reshape钩子hook??
  5. 定制onnx的输入节点shape

infer封装:

目的:实现tensorRT引擎的推理管理,自动关联引擎的输入和输出,管理上下文,插件

RAII+接口模式的封装

onnxPlugin封装:

目的:封装插件的细节,序列化,反序列化,creator,tensor,weight.

简单的plugin

onnx封装:(官方)

  1. protocol通过onnx-ml.proto编译得到onnx-ml.pb.h和onnx-ml.pb.cc

  2. onnx-ml.pb.cc的代码操作onnx模型文件,实现增删改

  3. onnx-ml.proto描述onnx文件如何组成,结构

推理发生错误解决方案:

onnx_parser:(官方)

其中builtin_op_importers.cpp对应onnx的插件

等价与tensorRT8x

封装后的代码

common

Onnxruntime

python:

session = onnxruntime.InferenceSession("workspace/yolov5s.onnx", providers=["CPUExecutionProvider"])
pred = session.run(["output"], {"images": image_input})[0]

c++:

openvino

intel在cpu上推理加速引擎

线程池

promise:

future:

condition_variable:

性能测量

时间:

#include 
auto startTime = std::chrono::high_resolution_clock::now();
context->enqueueV2(&buffers[0], stream, nullptr);
cudaStreamSynchronize(stream);
auto endTime = std::chrono::high_resolution_clock::now();
float totalTime = std::chrono::duration(endTime - startTime).count();

CUDA耗时:

计算CUDA事件之间的耗时

cudaEvent_t start, end;
cudaEventCreate(&start);	
cudaEventCreate(&end);
cudaEventRecord(start, stream); // 开始
context->enqueueV2(&buffers[0], stream, nullptr); // cuda事件
cudaEventRecord(end, stream);   // 结束
cudaEventSynchronize(end);		// 异步
float totalTime;
cudaEventElapsedTime(&totalTime, start, end); // 总耗时

遇见的问题

  1. Slice节点,提示slice is out of input range
    原因:Slice节点,是由yolo中Focus层导出所生成,生成时,其ends值【通常是-1】给定为极大的整数值,导致两者不兼容
    解决方案:

    • 修改pytorch的导出代码,让这个ends值是tensorRT合理的。修改的方式,是opset_version中找到对应的节点修改他
      修改/root/anaconda3/lib/python3.8/site-packages/torch/onnx/symbolic_opset11.py文件中slice函数
    • 干掉Focus层,使用cuda核去实现。把Focus层认为是预处理,与BGR->RGB转换、normalize进行合并为一个操作
  2. model.model[-1].export = True,指定为导出模式

    • model.model是所有layer的Sequential结构,model.model[-1]指最后一层,即Detect
    • model.model[-1].export = True,会使得Detect在forward时,返回原始数据,而不进行sigmoid等复原操作
      因为复原操作需要我们自己定义在cuda核中作为后处理实现
    • 后处理干的事情就是把网络输出结果回复成图像大小的框
    • 整个推理,就是 预处理(RGBBGR/FOCUS/Normalize) -> CNN(TensorRT) -> 后处理(Decode成框)
  3. Gather的错误,While parsing node number 97 [Gather]:
    原因:依旧是PyTorch和TensorRT和Onnx之间没有统一的原因。要么他修改了新版本,要么你修改了新版本,反正就不一起改
    解决方案:

    • 干掉Gather,分析原因:
      • 出现的第一个场景是:Resize节点

        • 修改def symbolic_fn(g, input, output_size, *args):的返回值为
          channel, height, width
          scales = [1, 2, 2]
          return g.op(“Upsample”, input, scales_f=scales)
          这里的scales指Upsample的缩放倍数
          Gather是由symbolic_fn内的调用造成
          经过这个操作后,Reisze以及各种Gather操作合并为一个Upsample,去掉Gather
      • 出现的第二个场景是:Reshape和Transpose节点

        • 出现在网络的输出节点上,Detect模块上
        • 由于Detect的forward中,使用view函数,输入的参数是来自x[i].shape。shape会在导出onnx时进行跟踪并生成节点
          因此产生了Gather、shape、concat、constant等一系列多余节点
          将x[i].shape返回值,强制转换为int(python类型)时,可以避免节点跟踪和生成多余节点
  4. 维度问题,onnx中和pytorch导出可以是5个维度,但是tensorRT显示是4个维度

    • 原因:目前的框架内使用的是tensorRT3个维度版本(CHW),N是由用户推理指定(MaxBatchSize,也是enqueue对应的Batch参数);目前没有考虑5个维度情况,因此需要去掉5个维度的问题。如果使用多维度(5个维度),灵活度上去,复杂度会异常高
    • 解决方案:去掉5个维度的影响,这通常都是可以去掉的,在yolov5中,去掉reshape和transpose(也就是view和permute)
  5. 推理过程中反序列化报错

Serialization (Serialization assertion creator failed.Cannot deserialize plugin since corresponding IPluginCreator not found in Plugin Registry)

  • 原因:为了使用TensorRT的插件,libnvinfer_plugin.so库必须被加载,所有插件必须通过调用initLibNvinferPlugins注册.

  • 解决:初始化并登记所有TensorRT的plugins到Plugin Registry.添加initLibNvinferPlugin()

  1. 推理耗时计算
#include 
auto startTime = std::chorno::high_resolution_clock::now();
auto endTime   = std::chorno::high_resolution_clock::now();
float totalTime= std::chorno::duration(endTime - startTime).count();
  1. best.onnx含有nms,输出为[concatoutput_dim, 7],其中onnx转tensorrt文件后,输出维度为[100, 7],即每次推理会输出100X7个框。如何改成输出[框的个数,7],即输出维度第一维也为动态。
  • 解决:1.在导出ONNX模型时去掉后处理,尽量不引入自定义OP,然后导出ONNX模型, 之后再通过CUDA编程实现NMS计算。
  1. tensorrt的精度FP32,(Quadro P4000不支持FP16), 推理时间均为60ms左右,而采用onnx文件在python进行推理一张图片1.7ms左右. 采用tensorrt加速反而推理速度变慢了?
  • time.time()计时单位为秒
  1. 预处理未进行cuda加速。

  2. 采用不带nms的onnx转FP32精度的TensorRT的engine模型推理一张图片需要2ms左右,采用CUDA后处理部分需要35ms左右,如何对nms进行提速?

  • 解决:可尝试将nms融入onnx,采用训练后INT8量化,降低模型精度;或者基于TensorRT 8.0之后版本,在训练过程中进行量化

11.tensorrt推理后:采用size_t类型输入到模型中,float类型输出,当将gpu推理的结果复制到cpu上时耗时4000ms。

解决办法:
nms,输出为[concatoutput_dim, 7],其中onnx转tensorrt文件后,输出维度为[100, 7],即每次推理会输出100X7个框。如何改成输出[框的个数,7],即输出维度第一维也为动态。

  • 解决:1.在导出ONNX模型时去掉后处理,尽量不引入自定义OP,然后导出ONNX模型, 之后再通过CUDA编程实现NMS计算。
  1. tensorrt的精度FP32,(Quadro P4000不支持FP16), 推理时间均为60ms左右,而采用onnx文件在python进行推理一张图片1.7ms左右. 采用tensorrt加速反而推理速度变慢了?
  • time.time()计时单位为秒
  1. 预处理未进行cuda加速。

  2. 采用不带nms的onnx转FP32精度的TensorRT的engine模型推理一张图片需要2ms左右,采用CUDA后处理部分需要35ms左右,如何对nms进行提速?

  • 解决:可尝试将nms融入onnx,采用训练后INT8量化,降低模型精度;或者基于TensorRT 8.0之后版本,在训练过程中进行量化

11.tensorrt推理后:采用size_t类型输入到模型中,float类型输出,当将gpu推理的结果复制到cpu上时耗时4000ms。

解决办法:
使用更快的数据传输方式:可以尝试使用更快的数据传输方式,如CUDA的异步内存拷贝,或者使用更快的设备之间的数据传输方式,如PCIe Gen3 x16或NVLink等。

减小数据传输量:可以尝试减小数据传输量,如使用更小的数据类型(如半精度浮点数或整数量化)或仅传输需要的数据(如仅传输置信度最高的检测框)。

对推理过程进行优化:可以尝试优化推理过程中的计算流程,如使用TensorRT的优化算法、减少计算图中的计算节点数量等,以减少GPU计算时间。

使用更快的CPU:可以尝试使用更快的CPU,以加快将数据从GPU传输到CPU的速度。

减少CPU处理时间:可以尝试减少CPU处理时间,如使用多线程或异步方式处理数据,以减少CPU处理数据的时间。
TensorRT_第1张图片

你可能感兴趣的:(随笔,算法,人工智能,机器学习)