将CPU代码移植到GPU想要提高性能需要记住的一些规则

部署运行你感兴趣的模型镜像

通常简单地将 CPU 移植到 GPU 实际上会使其变慢。如果您想要提高性能,则需要记住一些规则,稍后我将详细介绍。

GPU 模块的开发尽可能地与其 CPU 对应模块相似。这使得移植过程更加容易。在编写任何代码之前,您需要做的第一件事是将 GPU 模块链接到您的项目,并包含模块的头文件。GPU 的所有函数和数据结构都在cv命名空间的gpu子命名空间中。您可以通过use namespace关键字将其添加到默认命名空间,或者通过 cv:: 明确地将其标记在任何地方以避免混淆。我稍后会做。

#include <opencv2/gpu.hpp>         // GPU 结构和方法

GPU 代表“图形处理单元”。它最初是为渲染图形场景而构建的。这些场景以某种方式建立在大量数据之上。然而,这些场景并非都以顺序方式相互依赖,而是可以并行处理它们。因此,GPU 将包含多个较小的处理单元。这些不是最先进的处理器,在与 CPU 进行一对一测试时,它会落后。然而,它的优势在于它的数量。近年来,在非图形场景中利用 GPU 的这些巨大并行能力的趋势日益增加;渲染也是如此。这催生了图形处理单元上的通用计算 (GPGPU)。

GPU 有自己的内存。当您使用 OpenCV 将硬盘中的数据读取到系统内存中的Mat对象中时。CPU 以某种方式直接在此基础上工作(通过其缓存),但 GPU 不能。它必须将计算所需的信息从系统内存传输到自己的内存中。这是通过上传过程完成的,非常耗时。最后,结果必须下载回您的系统内存,以便您的 CPU 查看和使用它。不建议将小函数移植到 GPU,因为上传/下载时间将大于您通过并行执行获得的时间。

Mat 对象仅存储在系统内存(或 CPU 缓存)中。要将 OpenCV 矩阵传输到 GPU,您需要使用其 GPU 对应项cv::cuda::GpuMat。它的工作原理类似于 Mat,但仅限于 2D,并且其函数没有返回引用(不能将 GPU 引用与 CPU 引用混合)。要将 Mat 对象上传到 GPU,您需要在创建类的实例后调用上传函数。要下载,您可以使用对 Mat 对象的简单分配或使用下载函数。

Mat I1;         // Main memory item - read image into with imread for example
gpu::GpuMat gI; // GPU matrix - for now empty
gI1.upload(I1); // Upload a data from the system memory to the GPU memory
 
I1 = gI1;       // Download, gI1.download(I1) will work too

将数据存入 GPU 内存后,您可以调用 OpenCV 的 GPU 启用函数。大多数函数的名称与 CPU 上的名称相同,不同之处在于它们仅接受GpuMat输入。

要记住的另一件事是,并非所有通道号都可以在 GPU 上制作有效的算法。通常,我发现 GPU 图像的输入图像需要是一通道或四通道图像,并且项目大小需要是 char 或 float 类型之一。抱歉,GPU 不支持双重。为某些函数传递其他类型的对象将导致抛出异常,并在错误输出中显示错误消息。文档在大多数地方详细说明了输入所接受的类型。如果您有三个通道图像作为输入,您可以做两件事:添加新通道(并使用 char 元素)或拆分图像并为每个图像调用函数。第一个并不推荐,因为这会浪费内存。

对于某些函数,元素(相邻项)的位置无关紧要,快速解决方案是将其重塑为单通道图像。这是 PSNR 实现的情况,对于absdiff方法,邻居的值并不重要。但是,对于GaussianBlur,这不是一个选项,因此需要对 SSIM 使用分割方法。有了这些知识,您可以编写一个 GPU 可行代码(例如我的 GPU 代码)并运行它。您会惊讶地发现它可能比您的 CPU 实现慢。

优化

这样做的原因是您将内存分配和数据传输的代价抛在了一边。而在 GPU 上,这个代价实在是太高了。另一种优化的可能性是借助 cv ::cuda::Stream引入异步 OpenCV GPU 调用。

  1. GPU 上的内存分配相当可观。因此,如果可能的话,请尽可能少地分配新内存。如果您创建一个打算多次调用的函数,最好在第一次调用期间只为该函数分配一次任何本地参数。为此,您需要创建一个包含您将使用的所有本地变量的数据结构。例如,在 PSNR 的情况下,这些是:
struct BufferPSNR                                     // Optimized GPU versions
  {   // Data allocations are very expensive on GPU. Use a buffer to solve: allocate once reuse later.
  gpu::GpuMat gI1, gI2, gs, t1,t2;
 
  gpu::GpuMat buf;
};

然后在主程序中创建它的一个实例:BufferPSNR bufferPSNR;

最后,每次调用时将其传递给函数:

double getPSNR_GPU_optimized(const Mat& I1, const Mat& I2, BufferPSNR& b)
  1. 现在,您可以访问这些本地参数:b.gI1b.buf等。如果新矩阵大小与前一个矩阵大小不同,GpuMat 只会在新调用时重新分配自身。
  2. 避免不必要的函数数据传输。一旦进入 GPU,任何小的数据传输都将是重要的。因此,如果可能的话,请在原地进行所有计算(换句话说,不要创建新的内存对象 - 原因在上一点中已解释)。例如,虽然用一行公式表达算术运算可能更容易,但速度会更慢。在 SSIM 的情况下,我需要计算:

b.t1 = 2 * b.mu1_mu2 + C1;

虽然上面的调用会成功,但请注意,存在隐藏的数据传输。在进行加法之前,它需要将乘法存储在某处。因此,它将在后台创建一个本地矩阵,将C1值添加到该矩阵,最后将其分配给t1。为了避免这种情况,我们使用 gpu 函数,而不是算术运算符:

gpu::multiply(b.mu1_mu2, 2, b.t1); //b.t1 = 2 * b.mu1_mu2 + C1;
gpu::添加(b.t1,C1,b.t1);

使用异步调用(cv::cuda::Stream)。默认情况下,每当您调用 GPU 函数时,它都会等待调用完成,然后返回结果。但是,可以进行异步调用,这意味着它将调用操作执行,为算法进行昂贵的数据分配,然后立即返回。现在,如果您愿意,可以调用另一个函数。对于 MSSIM,这是一个小的优化点。在我们的默认实现中,我们将图像分成多个通道,并为每个通道调用 GPU 函数。使用流可以实现一定程度的并行化。通过使用流,我们可以在 GPU 执行给定方法时进行数据分配和上传操作。例如,我们需要上传两张图像。我们将它们一个接一个地排队,然后调用处理它的函数。函数将等待上传完成,但是在此过程中,它会为接下来要执行的函数分配输出缓冲区。

gpu::Stream stream;
 
stream.enqueueConvert(b.gI1, b.t1, CV_32F);    // Upload
 
gpu::split(b.t1, b.vI1, stream);              // Methods (pass the stream as final parameter).
gpu::multiply(b.vI1[i], b.vI1[i], b.I1_2, stream);        // I1^2

结果与结论

在英特尔 P8700 笔记本电脑 CPU 与低端 NVIDIA GT220M 搭配使用时,性能数据如下:

Time of PSNR CPU (averaged for 10 runs): 41.4122 milliseconds. With result of: 19.2506
Time of PSNR GPU (averaged for 10 runs): 158.977 milliseconds. With result of: 19.2506
Initial call GPU optimized:              31.3418 milliseconds. With result of: 19.2506
Time of PSNR GPU OPTIMIZED ( / 10 runs): 24.8171 milliseconds. With result of: 19.2506
 
Time of MSSIM CPU (averaged for 10 runs): 484.343 milliseconds. With result of B0.890964 G0.903845 R0.936934
Time of MSSIM GPU (averaged for 10 runs): 745.105 milliseconds. With result of B0.89922 G0.909051 R0.968223
Time of MSSIM GPU Initial Call            357.746 milliseconds. With result of B0.890964 G0.903845 R0.936934
Time of MSSIM GPU OPTIMIZED ( / 10 runs): 203.091 milliseconds. With result of B0.890964 G0.903845 R0.936934

在这两种情况下,与 CPU 实现相比,我们的性能提升了近 100%。这可能正是您的应用程序运行所需的改进。

您可能感兴趣的与本文相关的镜像

PyTorch 2.5

PyTorch 2.5

PyTorch
Cuda

PyTorch 是一个开源的 Python 机器学习库,基于 Torch 库,底层由 C++ 实现,应用于人工智能领域,如计算机视觉和自然语言处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值