基本 GPU 应用,整数计算

Basic GPU application, integer calculations

长话短说,我做过几个交互软件的原型。我现在使用 pygame(python sdl wrapper),一切都在 CPU 上完成。我现在开始将它移植到 C,同时搜索现有的可能性,以使用一些 GPU 能力从冗余操作中获得 CPU。但是我找不到一个好的 "guideline" 在我的情况下我应该选择什么 technology/tools 。我只是阅读过多的文档,它很快就会耗尽我的精神力量。我完全不确定这是否可能,所以我很困惑。
在这里,我对我开发的典型应用程序框架做了一个非常粗略的草图,但鉴于它现在使用 GPU(请注意,我对 GPU 编程的实践知识几乎为零)。仍然重要的是必须准确保留数据类型和功能。在这里:

所以F(A,R,P)是一些自定义函数,例如元素替换,重复等。函数在程序生命周期内大概是不变的,矩形的形状一般不等于A形状,所以它不是就地计算。所以它们只是用我的函数生成的。 F的例子:重复A的行和列;用 Substitution tables 中的值替换值;将一些图块组合成单个数组; A 值上的任何数学函数等。如前所述,所有这些都可以在 CPU 上轻松完成,但应用程序必须非常流畅。顺便说一句,在纯 Python 中,它在添加了几个基于 numpy 数组的视觉特征后变得不可用。 Cython 有助于制作快速的自定义函数,但源代码已经是一种沙拉了。

问题:

我知道,这是一个大问题,所以如果有帮助,我会提供更多细节。


更新

这是我的位图编辑器原型的两个典型计算的具体例子。因此,编辑器使用索引,数据包括具有相应位掩码的层。我可以确定图层和蒙版的大小与图层的大小相同,也就是说,所有图层的大小都相同(1024^2 像素 = 4 MB,用于 32 位值)。我的调色板是 1024 个元素(32 bpp 格式为 4 KB)。
考虑一下我现在想做两件事:

步骤 1。我想将所有图层合二为一。假设 A1 是默认图层(背景),图层 'A2' 和 'A3' 具有蒙版 'm2' 和 'm3'。在 python 我会写:

from numpy import logical_not
...
Result = (A1 * logical_not(m2) + A2 * m2) * logical_not(m3) + A3 * m3

由于数据是独立的,我相信它必须使加速与并行块的数量成比例。

步骤 2。现在我有一个数组,想用一些调色板 'colorize' 它,所以它将是我的查找 table。正如我现在看到的,同时读取 lookup table 元素存在问题。

但我的想法是,也许可以为所有块复制调色板,这样每个块都可以读取自己的调色板?像这样:

OpenCL/CUDA 之间没有太大区别,所以请选择更适合您的。请记住,CUDA 会将您限制在 NVidia GPU 上。

如果我正确理解你的问题,内核(在 GPU 上执行的函数)应该很简单。它应该遵循这个伪代码:

kernel main(shared A, shared outA, const struct R, const struct P, const int maxOut, const int sizeA)
  int index := getIndex() // get offset in input array
  if(sizeA >= index) return // GPU often works better when n of threads is 2^n
  int outIndex := index*maxOut // to get offset in output array
  outA[outIndex] := F(A[index], R, P)
end

函数 F 应该是内联的,您可以使用 switch 或 if 来实现不同的函数。由于 F 的输出大小未知,因此您必须使用更多内存。每个内核实例都必须知道正确内存写入和读取的位置,因此必须有一些最大大小(如果有 none,那么这一切都是无用的,你必须使用 CPU!)。如果不同的大小是稀疏的,那么我会在将数组返回到 RAM 后使用类似计算这些不同大小的方法,并使用 CPU 计算这些大小,同时用一些零或指示值填充 A。

数组的大小显然是 length(A) * maxOut = length(outA)。

我忘了说,如果在大多数情况下 F 的执行不相同(相同的源代码),那么 GPU 将序列化它。 GPU 多处理器有几个核心连接到同一个指令缓存中,所以它必须序列化代码,这对所有核心来说都是不一样的! OpenMP 或线程是解决此类问题的更好选择!

当您的代码是高度并行的(即处理阶段之间的数据依赖性很小或没有)时,您可以选择 CUDA(对同步进行更细粒度的控制)或 OpenCL(非常相似且可移植的 OpenGL-like API 与 GPU 接口以进行内核处理)。我们所做的大部分加速工作都是在 OpenCL 中进行的,它与 OpenGL 和 DirectX 具有出色的互操作性,但我们也有与 CUDA 一起使用的相同设置。 CUDA 和 OpenCL 之间的一大区别是,在 CUDA 中,您可以编译一次内核并在您的应用程序中延迟加载(and/or link)它们,而在 OpenCL 中,编译器与 OpenCL 驱动程序堆栈配合得很好确保在应用程序启动时编译内核。

如果您使用 Microsoft Visual Studio,一个经常被忽视的替代方案是 C++AMP,它是一种 C++ 语法友好且直观的 api,适合那些不想深入研究OpenCL/CUDAAPI的逻辑曲折。这里的一大优势是,如果您的系统中没有 GPU,代码也可以工作,但是您没有那么多的选项来调整性能。尽管如此,在很多情况下,这是一种快速有效的方法来验证您的概念代码并稍后在 CUDA 或 OpenCL 中重新实现位和部分。

OpenMP 和 Thread Building Blocks 只是当您遇到同步问题和大量数据依赖时的好选择。使用工作线程的本机线程也是一个可行的解决方案,但前提是您对如何在不同进程之间设置同步点有一个很好的想法,这样线程在争夺优先级时不会互相饿死。要做到这一点要困难得多,Parallel Studio 等工具是必须的。但是,如果您正在编写 GPU 代码,NVida NSight 也是如此。

附录:

正在开发一个名为 Quasar (http://quasar.ugent.be/blog/) 的新平台,使您能够使用与 Matlab 非常相似的语法编写数学问题,但完全支持 c/c++ /c# 或 java 集成,并将您的 "kernel" 代码交叉编译(LLVM、CLANG)到任何底层硬件配置。它生成 CUDA ptx 文件,或在 openCL 上运行,甚至在使用 TBB 的 CPU 上运行,或它们的混合。使用一些名字,您可以修饰算法,以便底层编译器可以推断类型(您也可以显式使用严格类型),这样您就可以将类型繁重的事情完全留给编译器。公平地说,在撰写本文时,系统仍然是 w.i.p。并且第一个 OpenCL 编译程序刚刚被测试,但最重要的好处是快速原型制作,与优化的 cuda 相比性能几乎相同。

您想要做的是使用高频分派非常快速地向 GPU 发送值,然后显示基本上是纹理查找和一些参数的函数的结果。

我会说这个问题只有在满足两个条件时才值得在 GPU 上解决:

  1. A[] 的大小经过优化,使传输时间无关紧要(看,http://blog.theincredibleholk.org/blog/2012/11/29/a-look-at-gpu-memory-transfer/)。

  2. 查找 table 不太大 and/or 查找值的组织方式可以最大限度地利用缓存,一般来说,GPU 上的随机查找可以缓慢,理想情况下,您可以为 A[] 缓冲区的每个元素在共享内存缓冲区中预加载 R[] 值。

如果您可以肯定地回答这两个问题,然后才考虑尝试使用 GPU 来解决您的问题,否则这两个因素将压倒 GPU 可以为您提供的计算加速。

您可以查看的另一件事是尽可能重叠传输和计算时间,以尽可能隐藏 CPU->GPU 数据的缓慢传输速率。

关于你的 F(A, R, P) 函数,你需要确保你不需要知道 F(A, R, P)[0] 的值来知道 F(A, R, P)[1] 的值是什么,因为如果你那么你需要重写 F(A, R, P) 来解决这个问题,使用一些并行化技术。如果您的 F() 函数数量有限,那么这可以通过编写每个 F() 函数的并行版本供 GPU 使用来解决,但是如果 F() 是用户定义的,那么您的问题变得有点棘手。

我希望这些信息足以让您对是否应该使用 GPU 来解决问题有一个明智的猜测。

编辑

阅读您的编辑后,我会同意。调色板可以放入共享内存(参见 GPU shared memory size is very small - what can I do about it?),这非常快,如果您有多个调色板,则可以容纳 16KB(大多数卡上共享内存的大小)/每个调色板 4KB = 每个块 4 个调色板线程数。

最后一个警告,整数运算在 GPU 上不是最快的,如果有必要,请在实现算法后考虑使用浮点数,并且它是一种廉价的优化。