TensorRT推理过程及多线程性能分析

背景知识:

CPU(Central Processing Unit,中央处理器)和GPU(Graphic Processing Unit,图形处理器)的区别和协同工作流程:

  cpu架构

  CPU 基于低延时的设计:有强大的ALU(算术运算单元),复杂的Control(逻辑控制单元)和大的Cache(缓存单元,用于保存计算数据)(图中的DRAM是电脑的运行内存)。用于解释计算机指令以及处理计算机软件中的数据,其中需要很强的通用性来处理各种不同的数据类型,同时又要逻辑判断又会引入大量的分支跳转和中断的处理。这些都使得CPU的内部结构异常复杂。

gpu架构

  GPU是基于大的吞吐量设计:GPU则有大量的ALU和少量的Cache(不是用来保存计算数据,而是当多线程访问同一个数据时,缓存并合并这些访问,然后再去访问DRAM,获取数据后再转发数据给对应的线程),通过分配多个threads,可以实现大规模并发计算。在神经网络训练过程中,运算最多的是关于矩阵的运算,这个时候就正好用到了GPU,GPU本来是用来处理图形的,但是因为其处理矩阵计算的高效性就运用到了深度学习之中。

  协同工作流程:

  1. 程序加载:操作系统或应用程序将需要的数据和指令加载到内存中。
  2. CPU 控制流:CPU 控制着整个系统的指令流,它读取内存中的指令,解析并执行它们。
  3. 数据处理
    • 对于适合并行处理的任务,CPU 可能会将部分数据和计算任务卸载到 GPU 上。
    • GPU 在其本地内存中接收数据,使用大量的并行计算单元处理数据。
    • 处理完成后,结果可能被写回到主内存,或者直接用于进一步的 GPU 计算。
  4. 结果输出:最终结果由 CPU 或 GPU 处理后,存储在内存中,然后可能被写入硬盘或显示在屏幕上.

  CPU 和 GPU 通过 PCIe 总线进行通信,而数据实际上存储在内存中。PCIe 总线作为物理层的桥梁,允许 GPU 使用 DMA(Direct Memory Access) 技术直接访问内存,同时也支持更高级的技术如 NVLink(用于 GPU 和 CPU 之间的数据传输)和 GPUDirect(允许 GPU 直接访问其他设备的内存)来优化特定场景下的通信效率。

 

TensorRT:是 NVIDIA 开发的一个高性能的深度学习推理优化库,专为加速深度神经网络的推理过程而设计,它通过对模型进行优化(如层融合、内核优化、半精度计算支持等),并利用 GPU 的硬件特性(如CUDA核心,Tensor Cores,高带宽显存等),来提高推理速度和能效。

 

TensorRT推理过程:

  首先是根据模型分配合理的输入数据:
  (这是onnx模型,可以看到INPUTS的格式)

BATCH_SIZE = 1;
IMAGE_HEIGHT = 640;
IMAGE_WIDTH = 640;
IMAGE_CHANNEL = 3;
MAX_IMAGE_SIZE = 3000 * 3000;
NUM_BOXES = 8400;  //每个图像最多检测的边界框数
NUM_OBJECTS = 1000;  //每个类别最多检测的边界框数

  然后读取engine文件,分配数据内存,创建CUDA流

readEngineFile(ENGINE_FILE, engine);  //读取引擎文件
context = engine->createExecutionContext();  //创建执行上下文对象

void* buffers[2];  //创建一个指针数组buffers,用于存储输入和输出数据
uint8_t* image_device = nullptr;  //创建一个指针image_device,用于存储图像数据
const int input_size = BATCH_SIZE * IMAGE_HEIGHT * IMAGE_WIDTH * IMAGE_CHANNEL * sizeof(float);  //输入数据大小
const int output_size = BATCH_SIZE * NUM_BOXES * (NUM_CLASSES + 4) * sizeof(float);  //输出数据大小,对于每个边界框,除了类别概率之外,还需要存储4个坐标值。
const int max_size = BATCH_SIZE * MAX_IMAGE_SIZE * IMAGE_CHANNEL;  //最大图像数据大小
const int input_index = engine->getBindingIndex("images");  //获取输入索引
const int output_index = engine->getBindingIndex("output");	 //获取输出索引
cudaMalloc(&buffers[input_index], input_size);  //分配输入数据内存,&buffers[input_index]为输入数据(void** devPtr类型)的地址
cudaMalloc(&buffers[output_index], output_size);
cudaMalloc((void**)&image_device, max_size); //分配图像数据内存,&image_device(uint8_t** devPtr类型)为图像数据(uint8_t* devPtr类型)的地址,发生了类型转换
auto* engine_output = new float[NUM_BOXES * (NUM_CLASSES + 4) * BATCH_SIZE];

//在 CUDA 编程中,流(stream)是实现异步操作和并发执行的重要机制。通过创建多个流,可以在不同的流中安排不同的 CUDA 操作,从而实现这些操作的并发执行。
cudaStream_t stream;  //声明一个 CUDA 流变量 stream
cudaStreamCreate(&stream); //创建一个新的 CUDA 流,并将流的句柄存储在 stream 变量中

  最后开始对图像进行推理,一共有5个步骤

  首先将图像数据从主机Host(CPU)内存异步复制到设备Device(GPU)内存,异步复制操作允许CPU和GPU并行工作,提高了整个系统的利用率和性能。

size_t image_size = image.rows * image.cols * image.channels();
cudaMemcpyAsync( //将图像数据从主机(CPU)内存异步复制到设备(GPU)内存
	image_device,  //目标地址
	image.data,  //源地址
	image_size,  //数据大小
	cudaMemcpyHostToDevice,  //数据方向:CPU -> GPU
	stream  //流
);

  然后根据onnx模型的输入要求对图像数据进行预处理(缩放,归一化,通道转换,填充,在GPU上执行),以便它可以在GPU上进行进一步的处理或计算

preProcess(image_device, image.rows, image.cols, scale, (float*)buffers[input_index], IMAGE_HEIGHT, IMAGE_WIDTH);

  接着在GPU上异步地执行CUDA核函数,buffers用来读取输入数据和储存输出数据;stream是逻辑队列,允许在GPU上并行执行多个异步操作。

context->enqueueV2(buffers, stream, nullptr);

  然后将模型推理的输出数据从设备Device(GPU)内存异步复制到主机Host(CPU)内存,并确保所有相关的GPU操作都已经完成。

cudaMemcpyAsync( //将输出数据从设备(GPU)内存异步复制到主机(CPU)内存
	engine_output,  //目标地址
	buffers[1],  //输出缓冲区
	output_size,  //数据大小
	cudaMemcpyDeviceToHost, // GPU -> CPU
	stream
);
cudaStreamSynchronize(stream);  //同步流,等待流中的所有操作完成

  最后对模型推理的输出数据进行后处理(解析输出数据,置信度过滤,非极大值抑制,坐标转换,类别标签映射,在CPU上执行),储存在detections向量中。

std::vector<YOLO::DetectRet> detections;
postProcess(engine_output, output_size, NUM_BOXES, NUM_CLASSES, CONF_THRESHOLD, NMS_THRESHOLD, 1 / scale, NUM_OBJECTS, LABELS, detections);

 

多线程性能分析:
 
首先来看看不同线程(从1到8)下的推理时间对比:
 

  不难看出,随着线程数的增加,推理时间也在不断增加。经过估算,对于我的4060显卡而言,相同吞吐量下用时最短的是2个线程或3个线程。

  从TensorRT的推理过程分析,随着线程数的增加,从CPU到GPU的数据传输开销会变大,导致时间增加。

  从GPU工作原理来看,每个线程都需要访问GPU的计算资源,当资源成为瓶颈时,线程必须等待资源可用,从而增加了推理时间;还有线程调度和同步,这可能会导致额外的开销。

  但实验过程我发现了一个有意思的现象,当我把线程数从8到16到24这样越级地跳转时,CPU的占用从20%逐渐上升至40%多,这可以从复制图像数据和后处理的开销解释,但GPU却一直维持在12%左右,按理来说GPU也应该逐渐上升才对,难道是计算资源已经到达瓶颈?只有12%的占用也说不过去,那么应该是数据传输的瓶颈,于是我换了个数据集,将原来的10几kb大小的图片换成了2-4mb大小的图片。

  在单线程下,CPU占用10%,GPU占用9%;

  在2线程下,CPU占用16%,GPU占用18%;

  在3线程下,CPU占用23%,GPU占用22%;

  在4线程下,CPU占用30%,GPU占用22%;

  在8线程下,CPU占用55%,GPU占用22%

  结果表明在2,3线程CPU与GPU之间的数据传输已经快要达到瓶颈,这似乎恰好对于上述”相同吞吐量下用时最短的是2个线程或3个线程。“

posted @ 2024-07-23 18:15  earththreebody  阅读(1361)  评论(0)    收藏  举报