【问题标题】:In CUDA, what is memory coalescing, and how is it achieved?在 CUDA 中,什么是内存合并,它是如何实现的?
【发布时间】:2011-02-18 12:33:15
【问题描述】:

什么是 CUDA 全局内存事务中的“合并”?即使通过我的 CUDA 指南,我也无法理解。怎么做?在 CUDA 编程指南矩阵示例中,逐行访问矩阵称为“合并”或 col.. by col.. 称为合并? 哪个是正确的,为什么?

【问题讨论】:

    标签: cuda definition memory-access


    【解决方案1】:

    此信息可能仅适用于计算能力 1.x 或 cuda 2.0。更新的架构和 cuda 3.0 具有更复杂的全局内存访问,实际上甚至没有为这些芯片配置“合并的全局负载”。

    另外,这个逻辑也可以应用到共享内存中来避免存储库冲突。


    合并内存事务是一个半扭曲中的所有线程同时访问全局内存的事务。这太简单了,但正确的做法是让连续的线程访问连续的内存地址。

    因此,如果线程 0、1、2 和 3 读取全局内存 0x0、0x4、0x8 和 0xc,则应该是合并读取。

    在矩阵示例中,请记住您希望矩阵线性驻留在内存中。您可以根据需要执行此操作,并且您的内存访问应反映矩阵的布局方式。所以,下面的 3x4 矩阵

    0 1 2 3
    4 5 6 7
    8 9 a b
    

    可以像这样逐行完成,以便 (r,c) 映射到内存 (r*4 + c)

    0 1 2 3 4 5 6 7 8 9 a b
    

    假设您需要访问一次元素,并假设您有四个线程。哪些线程将用于哪个元素?可能是

    thread 0:  0, 1, 2
    thread 1:  3, 4, 5
    thread 2:  6, 7, 8
    thread 3:  9, a, b
    

    thread 0:  0, 4, 8
    thread 1:  1, 5, 9
    thread 2:  2, 6, a
    thread 3:  3, 7, b
    

    哪个更好?哪些会导致合并读取,哪些不会?

    无论哪种方式,每个线程都会进行 3 次访问。让我们看一下第一次访问,看看线程是否连续访问内存。在第一个选项中,第一次访问是 0、3、6、9。不连续,不合并。第二个选项,是0、1、2、3。连续!合并!耶!

    最好的方法可能是编写您的内核,然后对其进行分析以查看您是否有未合并的全局加载和存储。

    【讨论】:

    • 感谢您查看哪个线程访问哪个元素的解释。目前我有第一个选择 (thread 0: 0, 1, 2 etc...) 所以我现在正在寻找更好的选择:-)
    • @jmilloy - 我想问一下如何分析内核以查看未合并的全局加载和存储。
    • @muradin 你可以使用 Visual Profiler 吗? developer.nvidia.com/nvidia-visual-profiler
    • @jmilloy - 因为我在非图形环境中工作,所以我在命令行模式下搜索并找到了 nvprof。但是当我想运行它时出现错误:nvprof 无法加载 libcuda.so.1 没有这样的文件或目录!你知道为什么吗?
    • @jmilloy:你好,非常好的例子!谢谢!我想问你,当你说你可以运行分析器来查看你是否已经合并或不访问时,你怎么做?对于示例,运行:nvprof --metrics gld_efficiency ?而且越高越好?
    【解决方案2】:

    内存合并是一种允许优化使用全局内存带宽的技术。 也就是说,当并行线程运行相同的指令访问全局内存中的连续位置时,实现了最有利的访问模式。

    上图中的示例有助于解释合并排列:

    在图 (a) 中,长度为 mn 个向量以线性方式存储。向量j的元素ivji表示。 GPU 内核中的每个线程都分配给一个 m 长度的向量。 CUDA 中的线程被分组在一个块数组中,GPU 中的每个线程都有一个唯一的 id,可以定义为indx=bd*bx+tx,其中bd 表示块维度,bx 表示块索引,tx 是线程每个块中的索引。

    垂直箭头表示并行线程访问每个向量的第一个组件的情况,即内存的地址 0、m2m...。如图(a)所示,在这种情况下,内存访问不是连续的。通过将这些地址之间的间隙归零(上图中的红色箭头),内存访问就会合并。

    但是,这里的问题有点棘手,因为每个 GPU 块允许的驻留线程大小限制为 bd。因此,可以通过以连续顺序存储第一个bd 向量的第一个元素,然后是第二个 bd 向量的第一个元素,依此类推来完成合并数据排列。其余的向量元素以类似的方式存储,如图(b)所示。如果 n(向量数)不是bd 的因子,则需要用一些微不足道的值填充最后一个块中的剩余数据,例如0.

    在图(a)的线性数据存储中,向量的分量i(0≤im)索引 (0 ≤ indx n) 由m × indx +i 寻址;合并中的相同组件 图(b)中的存储模式被寻址为

    (m × bd) ixC + bd × ixB + ixA,

    ixC = floor[(m.indx + j )/(m.bd)]= bxixB = jixA = mod(indx,bd) = tx

    综上所述,在存储多个大小为 m 的向量的示例中,线性索引根据以下方式映射到合并索引:

    m.indx +i −→ m.bd.bx +i .bd +tx

    这种数据重新排列会导致 GPU 全局内存的显着更高的内存带宽。


    来源:“非线性有限元变形分析中基于 GPU 的计算加速。”生物医学工程数值方法国际期刊(2013 年)。

    【讨论】:

      【解决方案3】:

      如果块中的线程正在访问连续的全局内存位置,则所有访问都由硬件组合成一个请求(或合并)。在矩阵示例中,行中的矩阵元素线性排列,然后是下一行,依此类推。 例如,一个块中的 2x2 矩阵和 2 个线程,内存位置排列为:

      (0,0) (0,1) (1,0) (1,1)

      在行访问中,thread1 访问不能合并的 (0,0) 和 (1,0)。 在列访问中,thread1访问的是(0,0)和(0,1),因为它们是相邻的,所以可以合并。

      【讨论】:

      • 简洁明了,但是.. 请记住,coalesced 不是关于 thread1 的两个串行访问,而是 thread1 和 thread2 并行的同时访问。在您的行访问示例中,如果 thread1 访问 (0,0) 和 (1,0),那么我假设 thread2 正在访问 (0,1) 和 (1,1)。因此,第一个并行访问是 1:(0,0) 和 2:(0,1) --> 合并!
      【解决方案4】:

      合并的标准在CUDA 3.2 Programming Guide,第 G.3.2 节中有很好的记录。简短的版本如下:warp 中的线程必须按顺序访问内存,并且正在访问的单词应该 >=32 位。此外,warp 访问的基地址应分别为 64、128 或 256 字节对齐,以分别用于 32、64 和 128 位访问。

      Tesla2 和 Fermi 硬件在合并 8 位和 16 位访问方面做得不错,但如果您想要峰值带宽,最好避免使用它们。

      请注意,尽管 Tesla2 和 Fermi 硬件有所改进,但合并绝不意味着过时。即使在 Tesla2 或 Fermi 类硬件上,未能合并全局内存事务也可能导致 2 倍的性能损失。 (在 Fermi 类硬件上,这似乎仅在启用 ECC 时才成立。连续但未合并的内存事务对 Fermi 造成大约 20% 的打击。)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-04-14
        • 2021-08-31
        • 2021-08-15
        • 1970-01-01
        • 2010-10-10
        • 1970-01-01
        • 2010-09-13
        相关资源
        最近更新 更多