优化内存访问OpenCL

时间:2014-11-09 00:51:12

标签: opencl gpu

我目前正在使用块矩阵乘法算法在openCL内核中乘以字节矩阵:我将矩阵细分为块(32 x 32),将这些块加载到本地内存中,然后将其写回全局内存。

目前,内存访问是瓶颈。我正试图看看我能用它来优化它。

假设我在乘以C = A x B,其中A,B,C为char *

A(Ndim,Pdim),B(Pdim,MDim),C(Ndim,MDim)。

我目前有A行主格式和B列主格式,以确保内存访问在每个矩阵的工作组中是连续的。

每个工作项将一个字节加载到本地内存中,并负责处理该字节。我的内核的维度是{Ndim,Mdim}用于全局工作项,{block_size,block_size}用于本地工作项。

代码几乎与http://www.nvidia.com/content/cudazone/download/OpenCL/NVIDIA_OpenCL_ProgrammingGuide.pdf完全相同(除了A以列主格式存储)

我的问题:如何优化内存访问?我听到很多关于合并的内容,但我很难理解合并和并行之间的权衡。

选项0 :保持原样,即使每个线程访问一个字节,这也会合并,因此工作组中的每个线程都会获取已经访问过的数据。 - >不太可能,因为我的访问不是字节对齐的。我怀疑我每次都会加载 4个字节+ x,其中x是线程的偏移量。

选项1 :使用整数矩阵减少并行度 如果我将矩阵作为整数,我将能够一次加载更多,但会显着降低并行度(4倍),其中每个字节乘法必须按顺序执行。

选项2 :使用整数矩阵但保持并行性相同 这基本上意味着内存中的数据将被多次加载 直观地,这对应于int foo = get_global_id(0),然后,假设 我将foo转换为char [] foo_bytes,其字节为x = foo [get_local_id(0)); 我的理解是第一个线程将使用get_global_id(0)将数据加载到内存中,而工作组中的剩余线程将看到它已经加载

选项3 :使用整数矩阵,减少并行度,但使用向量类型 在工作项中处理数据 我知道opencl支持矢量类型,如果我加载一个32位整数,我可以转换 这是一个矢量类型,以便工作项将并行处理4个字节。 我的理解是,这只是语法,并且我不会在OpenCL中使用类似矢量类型的任何性能提升。

据我所知,选项2更可取。它是否正确?如果没有,为什么?

2 个答案:

答案 0 :(得分:1)

选项0 - 如果它保持代码简单并且您当前的性能足够好,那么这并不是那么糟糕。

选项1 - 我认为值得一试。您希望将4个字节作为单个int加载,并使用单个线程处理它。此ALU饱和度正是您的调度程序需要隐藏您遇到的全局内存延迟所需的。我认为这是选项#2非常接近的第二位。

选项2 - 可能是您提到的最好的选项,因为它将利用许多现代设备上提供的内存广播。每4个线程将读取一次int值。我认为,当每4个线程处理超过1个int时,测试性能是值得的(每4个线程可能有4个int,总共16个字节)。

选项3 - 这似乎是选项#1的自然延伸。如果你要给选项1一个镜头,那么将值映射到向量是下一个要测试的合乎逻辑的事情。可能每个架构都没有性能提升 - GPU喜欢浮点数,双精度数和整数,不一定是字节数。

更多提示/评论

我认为全球访问性能的最大优化是您已经实施的列主要订购。

你有没有使用half和halfn类型?对于支持一半的设备,您应该能够使浮点数/浮点数的数据密度加倍。这不如包装为int或char4的4个字节好,但任何支持half类型的设备都可能支持dot(halfn,halfn),这可以让你一次计算4,8或16个MAD。

选项4 - 我强烈建议您将更大的块读入本地内存。从本地存储器中乘32x32矩阵时,每个元素读取32次,但只从全局存储器读取一次。对64x64块执行相同操作时,元素将从本地内存中读取64次。 OpenCL设备具有32KB的共享内存,当您有三个32x32字节矩阵时,您只使用3KB。

如果你想使用方块:3 * 64x64字节= 12KB,3 * 96x96 = 27KB

如果您更喜欢使用输出矩阵“C”的32x32:

blockDim = ((32768 - 32*32) /2 )/32 = 496
1) read 496x32 block from A, store locally
2) read 496x32 block from B, store locally
3) read or initialize 32x32 block of C in local memory
4) do the math
5) write the 32x32 block to global memory C

496比大多数工作组尺寸允许的大,但我个人更喜欢使用32x1工作项并循环遍历数据。

答案 1 :(得分:1)

Memory coalescing is the single most important performance consideration用于编程nVidia GPU。如果线程 i 正在从内存位置 n 读取,则从位置 n + 1 读取线程 i + 1 。如果线程处于相同的warp中,则这些读取将“合并”到一个事务中。

请注意,在将每个子矩阵加载到共享内存的nVidia示例中,矩阵都是行主要顺序。这意味着(row,col)的线程将读取内存单元 row x stride + col (row,col + 1)的线程将读取内存单元行x stride + col + 1 ,它们确实在内存中彼此相邻。如果线程处于相同的warp中,则这将是coelesced - 这可能是since the threads are ordered in row-major order.

如果矩阵处于列主要顺序这就是一切了! (row,col + 1)的线程将读取 col x stride + row旁边的内存单元(col + 1)x stride + row 在记忆中!

因此,您对列主要订单的微小改变打破了在nVidia GPU中优化的最重要的事情!