TensorRT推理过程及多线程性能分析
背景知识:
CPU(Central Processing Unit,中央处理器)和GPU(Graphic Processing Unit,图形处理器)的区别和协同工作流程:

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

GPU是基于大的吞吐量设计:GPU则有大量的ALU和少量的Cache(不是用来保存计算数据,而是当多线程访问同一个数据时,缓存并合并这些访问,然后再去访问DRAM,获取数据后再转发数据给对应的线程),通过分配多个threads,可以实现大规模并发计算。在神经网络训练过程中,运算最多的是关于矩阵的运算,这个时候就正好用到了GPU,GPU本来是用来处理图形的,但是因为其处理矩阵计算的高效性就运用到了深度学习之中。
协同工作流程:
- 程序加载:操作系统或应用程序将需要的数据和指令加载到内存中。
- CPU 控制流:CPU 控制着整个系统的指令流,它读取内存中的指令,解析并执行它们。
- 数据处理:
- 对于适合并行处理的任务,CPU 可能会将部分数据和计算任务卸载到 GPU 上。
- GPU 在其本地内存中接收数据,使用大量的并行计算单元处理数据。
- 处理完成后,结果可能被写回到主内存,或者直接用于进一步的 GPU 计算。
- 结果输出:最终结果由 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个线程。“

浙公网安备 33010602011771号