跳转到内容
ForeverYoung
返回

通过NPP加速TensorRT部署时图片数据预处理

TensorRT(TRT)是NVIDIA推出的一个高性能的深度学习推理框架,可以让深度学习模型在NVIDIA GPU上实现低延迟,高吞吐量的部署。主流框架的模型可以通过转换为TensorRT在NVIDIA GPU进而达到极大地提速。然而,由于TensorRT并不支持常见的图片数据类型uint8,这使得往往需要在cpu上进行图片数据预处理,转换为其所支持的float并传入到gpu模型输入。当图片较大时,数据在cpu上的处理和传递时间较慢。本文将介绍如何通过cuda中的npp库来加速这一过程。

基本环境

SYS: Ubuntu 18.04
GPU: T4
GCC: 7.5
CMake: 3.16.6
CUDA: 10.2
CUDNN: 7.6.5
TensorRT: 7.0
OpenCV: 4.3.0/3.4.10

模型是由ssds.pytorch训练的Yolov3目标检测器,已转换为TRT模型。其输入大小为1x3x736x1280,特征提取器为ResNet18,计算精度为int8。

需要注意,当将模型转换为TRT模型时,TRT会根据gpu框架和性能来来选择不同的核函数及其参数,进而最大程度优化推理速度。因此TRT在执行时,必须使用统一gpu框架所生成的TRT模型,否则将无法推断。并且即使是统一框架不同型号的gpu所生成的TRT模型,其推理速度也会有些许削弱。比如虽然2080ti和t4同属7.5计算框架,在T4上推断,由2080ti所生成的TRT模型要比由T4生成的模型推断速度慢3~10%。

CPU图片数据预处理

在一些深度学习框架中,在其推断时可以指定推断时所接受的数据类型,并将数据预处理的步骤在计算图中定义。如在tensorflow将权重转化为推断模型(frozen graph)时,可以通过tf.placeholder(dtype=tf.uint8, shape=input_shape, name='image_tensor')来指定推理时模型接受的数据类型。这种改变在推理时模型的操作在TensorRT上似乎不那么行得通。虽然TensorRT支持多种数据类型,并且输入输出接口的数据类型可由转换的onnx或uff文件决定,但当输入输出的数据类型改为除float32以外的其他类型时,常常无法成功转换为TRT推理模型。

TensorRT中这种输入输出类型的限制,对于计算机视觉的模型显得更加不友好。计算机视觉常处理的图像或视频在计算机中常存储为0~255的uint8数据,这种类型本身就不被TensorRT所支持,须将图片先转为float数据再输入到TensorRT。而在一些任务中,其输入模型的图片或视频片段分辨率较大,如4k或8k,uint8和float从cpu内存传输到gpu显存的速度差别就比较大。这些原因导致了在计算机视觉模型部署时,图片数据预处理和传输有时候成为了模型部署的瓶颈。

大多数github上的TRT项目在进行推断中的图片数据预处理常采用TensorRT官方示例中给出了一种在CPU图片数据预处理。笔者个人喜欢用OpenCV自带函数进行操作,这两种图片数据预处理的示例代码如下。

代码:CPU图片数据预处理

TensorRT官方示例图片数据预处理代码

bool SampleUffSSD::processInput(const samplesCommon::BufferManager& buffers)
{
    const int inputC = mInputDims.d[0];
    const int inputH = mInputDims.d[1];
    const int inputW = mInputDims.d[2];
    const int batchSize = mParams.batchSize;

    // Available images
    std::vector<std::string> imageList = {"dog.ppm", "bus.ppm"};
    mPPMs.resize(batchSize);
    assert(mPPMs.size() <= imageList.size());
    for (int i = 0; i < batchSize; ++i)
    {
        readPPMFile(locateFile(imageList[i], mParams.dataDirs), mPPMs[i]);
    }

    float* hostDataBuffer = static_cast<float*>(buffers.getHostBuffer(mParams.inputTensorNames[0]));
    // Host memory for input buffer
    for (int i = 0, volImg = inputC * inputH * inputW; i < mParams.batchSize; ++i)
    {
        for (int c = 0; c < inputC; ++c)
        {
            // The color image to input should be in BGR order
            for (unsigned j = 0, volChl = inputH * inputW; j < volChl; ++j)
            {
                hostDataBuffer[i * volImg + c * volChl + j]
                    = (2.0 / 255.0) * float(mPPMs[i].buffer[j * inputC + c]) - 1.0;
            }
        }
    }

    return true;
}

OpenCV图片数据预处理代码

int imageToTensor(const std::vector<cv::Mat> & images, float * tensor, const int batch_size, const float alpha, const float beta) {
    const size_t height = images[0].rows;
    const size_t width = images[0].cols;
    const size_t channels = images[0].channels();
    const size_t stridesCv[3] = { width * channels, channels, 1 };
    const size_t strides[4] = { height * width * channels, height * width, width, 1 };

    #pragma omp parallel for num_threads(c_numOmpThread) schedule(static, 1)
    for (int b = 0; b < batch_size; b++)
    {
        cv::Mat image_f;
        images[b].convertTo(image_f, CV_32F, alpha, beta);
        std::vector<cv::Mat> split_channels = {
                cv::Mat(images[b].size(),CV_32FC1,tensor + b * strides[0]),
                cv::Mat(images[b].size(),CV_32FC1,tensor + b * strides[0] + strides[1]),
                cv::Mat(images[b].size(),CV_32FC1,tensor + b * strides[0] + 2*strides[1]),
        };
        cv::split(image_f, split_channels);
    }
    return batch_size * height * width * channels;
}

OpenCV图片数据预处理运算速度(ms)

GPU(Precision)Image2FloatCopy2GPUInferenceGPU2CPU
t4(int8)2.530260.9354512.561430.0210528

如上例代码所示,在CPU图片数据预处理中,首先将数据转化为float类型并做归一化,然后将数据排列从NHWC转化为NCHW,最后将float型nchw图片数据传递给到TRT模型预留的gpu显存。可以看到,对于该模型图像预处理和传输的速度反而大于模型推断速度。这种cpu图片预处理方式无法高效使用gpu的性能,使得整个模型部署效率较低。

GPU-NPP图片数据预处理

上面提到,cpu图片数据预处理效率较低由两方面原因导致:其一,cpu将图像从uint8转化为float32的效率较低;其二,相较于uint8,float32从cpu传输到gpu数据量大四倍,传输效率较慢。所以比较朴素的提速想法就是将uint8数据传输到gpu,并由gpu完成从uint8转化为float32的转化。NPP就可以方便快速的实现如上过程。

NPP是nvidia推出的用于gpu加速2D图像和信号处理的cuda库,其本身就内嵌于cuda库中。其分为多个部分,可以在gpu上高效的进行数据类型转换,颜色变化,几何变化等功能。本例中采用其中的NPPC,NPPIDEI和NPPIAL部分来进行图片数据预处理中的uint8到float32的数据类型转化,NHWC到NCHW的通道变化,以及归一化操作。其具体代码如下。

代码:GPU-NPP图片数据预处理

NPP图片数据预处理代码

int imageToTensorGPUFloat(const std::vector<cv::Mat> & images, void * gpu_images, void * tensor, const int batch_size, const float alpha) {
    const int height = images[0].rows;
    const int width  = images[0].cols;
    const size_t channels = images[0].channels();
    const size_t stride = height * width * channels;
    const size_t stride_s = width * channels;
    const int dstOrder[3] = {2, 1, 0};
    Npp32f scale[3] = {alpha, alpha, alpha};
    NppiSize dstSize = {width, height};

    #pragma omp parallel for num_threads(c_numOmpThread) schedule(static, 1)
    for (int b = 0; b < batch_size; b++)
    {
        cudaMemcpy((Npp8u*)gpu_images + b * stride, images[b].data, stride, cudaMemcpyHostToDevice);
        nppiSwapChannels_8u_C3IR((Npp8u*)gpu_images + b * stride, stride_s, dstSize, dstOrder);
        nppiConvert_8u32f_C3R((Npp8u*)gpu_images + b * stride, stride_s, (Npp32f*)tensor, stride_s*sizeof(float), dstSize);
        nppiMulC_32f_C3IR(scale, (Npp32f*)tensor, stride_s*sizeof(float), dstSize);
    }
    return batch_size * stride;
}

NPP图片数据预处理代码(无通道变化和归一化)

int imageToTensorGPUFloat(const std::vector<cv::Mat> & images, void * gpu_images, void * tensor, const int batch_size) {
    const int height = images[0].rows;
    const int width  = images[0].cols;
    const size_t channels = images[0].channels();
    const size_t stride = height * width * channels;
    NppiSize dstSize = {width, height};

    #pragma omp parallel for num_threads(c_numOmpThread) schedule(static, 1)
    for (int b = 0; b < batch_size; b++)
    {
        cudaMemcpy((Npp8u*)gpu_images + b * stride, images[b].data, stride, cudaMemcpyHostToDevice);
        nppiConvert_8u32f_C3R((Npp8u*)gpu_images + b * stride, width * channels, (Npp32f*)tensor, width * channels*sizeof(float), dstSize);
    }
    return batch_size * stride;
}

NPP图片数据预处理(无通道变化和归一化)运算速度(ms)

GPU(Precision)Image2GPU2FloatInferenceGPU2CPU
t4(int8)0.5324693.078690.0208867

如上例代码所示,在GPU图片数据预处理中,首先将uint8数据传输到gpu显存中,然后将数据排列从NHWC转化为NCHW,最后转化为float类型并做归一化,归一化后的数据直接存储在TRT模型预留的gpu显存中。由于按位运算(elementwise)和通道转换(channel permute)在TRT模型中执行效率较高,可将预处理中的归一化和通道转换移到模型内计算。可以看到,相较于cpu的图像预处理,gpu图像预处理的时间从3.5ms降到0.5ms,整个模型总运行时间从6ms降到3.5ms,每秒帧处理量(fps)从166帧提升到了285帧,整体达到了1.7倍的提速。

需要注意的是,由于TRT模型转化时间较长,本文示例只测试batch为1时的执行速度。如果在部署时遇到batch较大而导致gpu图片预处理速度较慢,由于cuda代码执行和传输特性,可考虑整批图像一起从cpu内存拷贝到gpu并进行uint8到float32的转化,进而提高大batch情况下的GPU图片数据预处理处理速度。

参考

如果有TensorRT的项目,不妨来试试通过本文提到的GPU图片数据预处理的方法来提速模型推断流程的速度吧!


分享这篇文章:

上一篇
Numba: 简单装饰器加速python代码
下一篇
Numba: 通过python快速学习cuda编程