【问题标题】:Memory Coalescing vs. Vectorized Memory Access内存合并与向量化内存访问
【发布时间】:2019-11-19 19:56:24
【问题描述】:

我试图了解 NVIDIA GPU/CUDA 上的内存合并与 x86-SSE/C++ 上的矢量化内存访问之间的关系。

据我了解:

  • 内存合并是内存控制器的运行时优化(在硬件中实现)。完成warp的加载/存储需要多少内存事务是在运行时确定的。除非有完美的合并,否则扭曲的加载/存储指令可能是 issued repeatedly
  • 内存矢量化是一种编译时优化。矢量化加载/存储的内存事务数是固定的。每个向量加载/存储指令只发出一次。
  • 可合并的 GPU 加载/存储指令比 SSE 向量加载/存储指令更具表现力。例如,st.global.s32 PTX 指令可以存储到 32 个任意内存位置(warp 大小为 32),而movdqa SSE 指令只能存储到连续的内存块中。
  • CUDA 中的内存合并似乎保证了高效的矢量化内存访问(当访问可合并时),而在 x86-SSE 上,我们不得不希望编译器实际上对代码进行了矢量化(它可能无法这样做)或使用 SSE 内在函数手动矢量化代码,这对程序员来说更难。

这是正确的吗?我是否错过了一个重要方面(可能是线程屏蔽)?

现在,为什么 GPU 具有运行时合并功能?这可能需要额外的硬件电路。与 CPU 中的编译时合并相比,主要优势是什么?是否存在由于缺少运行时合并而更难在 CPU 上实现的应用程序/内存访问模式?

【问题讨论】:

  • 将内存合并表征为“运行时优化”并不真正正确。它更像是 GPU 的默认执行模型。发散是指当一条指令不能在一个扭曲中以锁步方式执行时会发生这种情况,当一条内存指令不能在单个事务中提供服务时就会发生这种情况
  • 合并发生(或不发生)作为经纱中每个线程提供给给定 LD 或 ST 指令的 LD/ST 单元的地址的函数。内存控制器查看实际地址并确定哪些可以组合成特定的高速缓存行或内存段,然后向这些行/段发出请求。由于在运行时(在一般情况下)之前无法知道 warp 中每个线程指示的地址,因此无法在编译时预先计算此活动。
  • 内存合并提供的寻址灵活性(与您的 movdqa 示例相比)允许 CUDA 程序员编写任意线程代码并期望功能正确的结果。这大概有一些价值。程序员被允许做低效的事情以简化编程,但有一个路线图可以最大限度地利用内存子系统的性能/效率。为程序员提供两种选择被认为是有价值的。

标签: cuda gpu cpu-architecture simd coalescing


【解决方案1】:

警告:我不太了解/了解 GPU 的架构/微架构。其中一些理解是从问题 + 其他人在 cmets / answers 中写的内容拼凑而成的。

GPU 让一条指令对多条数据进行操作的方式非常不同于 CPU SIMD。这就是为什么他们需要对内存合并的特殊支持。 CPU-SIMD 无法以需要的方式进行编程。

顺便说一句,在实际 DRAM 控制器参与之前,CPU 具有缓存以吸收对同一缓存行的多次访问。当然,GPU 也有缓存。


是的,内存合并基本上在运行时执行短向量 CPU SIMD 在编译时执行的操作,在单个“核心”内执行。 CPU-SIMD 等价物是收集/分散加载/存储,可以优化对相邻索引的单个广泛访问缓存。 现有 CPU 不这样做:每个元素分别访问缓存一次聚会。如果您知道许多索引将相邻,则不应使用收集负载;将 128 位或 256 位块随机放置到位会更快。对于所有数据都是连续的常见情况,您只需使用正常向量加载指令而不是聚集加载。

现代短向量 CPU SIMD 的重点是通过 fetch/decode/exec 管道提供更多工作而不使其在必须解码 + 跟踪 + exec 方面更宽每个时钟周期的 CPU 指令数。 使 CPU 流水线更宽会很快导致大多数用例的收益递减,因为大多数代码没有很多 ILP。

通用 CPU 在指令调度/乱序执行机制上花费了大量晶体管,因此仅将其扩大到能够并行运行更多微指令是不可行的。 (https://electronics.stackexchange.com/questions/443186/why-not-make-one-big-cpu-core)。

为了获得更多的吞吐量,我们可以提高频率、提高 IPC 并使用 SIMD 来完成乱序机器必须跟踪的每条指令/微指令的更多工作。 (我们可以在单个芯片上构建多个内核,但是它们之间的缓存一致互连 + L3 缓存 + 内存控制器很难)。现代 CPU 使用所有这些东西,因此我们获得了频率 * IPC * SIMD 的总吞吐量能力,如果我们使用多线程,则乘以内核数。它们不是彼此可行的替代品,它们是正交的事情,您必须全部通过 CPU 管道驱动大量 FLOP 或整数工作。

这就是为什么 CPU SIMD 具有宽的固定宽度执行单元,而不是为每个标量操作单独的指令。没有一种机制可以将一条标量指令灵活地馈送到多个执行单元。

利用这一点需要在编译时进行向量化,不仅是您的加载/存储,还包括您的 ALU 计算。如果您的数据不连续,则必须将其收集到 SIMD 向量中,或者使用标量加载 + 随机播放,或者使用 AVX2 / AVX512 收集采用基地址 + (缩放)索引向量的加载。


但 GPU SIMD 不同。它适用于 大量 并行问题,您对每个元素执行相同的操作。 “管道”可以非常轻量级,因为它不需要支持乱序执行或寄存器重命名,尤其是分支和异常。这使得只拥有标量执行单元而不需要处理来自连续地址的固定块中的数据变得可行。

这是两种截然不同的编程模型。它们都是 SIMD,但运行它们的硬件细节却大不相同。


每个向量加载/存储指令只发出一次。

是的,这在逻辑上是正确的。在实践中,内部结构可能会稍微复杂一些,例如AMD Ryzen 将 256 位向量操作分成 128 位两半,或者 Intel Sandybridge/IvB 仅用于加载和存储,同时具有 256 位宽 FP ALU。

在 Intel x86 CPU 上存在未对齐的加载/存储问题:在高速缓存行拆分时,必须(从保留站)重播 uop 以执行访问的另一部分(到另一高速缓存行) )。

在英特尔术语中,用于拆分负载的 uop 会调度两次,但只会发出一次 + 退出一次。

对齐的加载/存储,如movdqamovdqu(当内存恰好在运行时对齐时)只是对 L1d 缓存的一次访问(假设缓存命中)。除非您使用的 CPU 将向量指令解码为两半,例如 AMD 的 256 位向量。


但这些东西纯粹是在 CPU 内核内部,用于访问 L1d 缓存。 CPU 内存事务在整个高速缓存行中,具有回写式 L1d / L2 私有高速缓存,并在现代 x86 CPU 上共享 L3 - Which cache mapping technique is used in intel core i7 processor?(Intel 自 Nehalem,i3 的开始/i5/i7 系列,自 Bulldozer 以来的 AMD,我认为为它们引入了 L3 缓存。)

在 CPU 中,是回写式 L1d 缓存基本上将事务合并到整个缓存行中,无论您是否使用 SIMD。

SIMD 有助于在 CPU 内完成更多工作,以跟上更快的内存。或者对于数据适合 L2 或 L1d 缓存的问题,真正快速处理该数据。

【讨论】:

    【解决方案2】:

    内存合并与parallel 访问有关:当SM 中的每个内核将访问后续内存位置时,内存访问得到优化。

    反之亦然,SIMD 是单核优化:当向量寄存器充满操作数并执行 SSE 操作时,并行性在 CPU 内核内部,每个时钟周期对每个内部逻辑单元执行一次操作。

    但是您是对的:合并/取消合并的内存访问是运行时方面的。 SIMD 操作是编译进去的。我认为他们不能很好地比较。

    如果我要进行并行处理,我会将 GPU 中的合并与 CPU 中的内存预取进行比较。这也是一个非常重要的运行时优化——我相信它在使用 SSE 的幕后也很活跃。

    但是,在 Intel CPU 内核中没有任何类似于 colescing 的东西。由于缓存一致性,优化并行内存访问的最佳方法是让每个内核访问独立的内存区域。

    现在,为什么 GPU 具有运行时合并功能?

    图形处理针对在相邻元素上并行执行单个任务进行了优化。

    例如,考虑对图像的每个像素执行操作,将每个像素分配给不同的核心。现在很明显,您希望有一个最佳路径来加载将一个像素散布到每个核心的图像。

    这就是内存合并深埋在 GPU 架构中的原因。

    【讨论】:

    • 这个答案一开始很好,但随后做出了一些奇怪的类比/比较/结论。硬件预取器占据 Pentium III 或 Pentium 4 或任何其他现代无序 x86 的总芯片面积的 50% 是不合理的。它们有大量的片上高速缓存(可能至少占用 50% 的片面积)。或许你就是这么想的?因为硬件预取确实会进入缓存。如果不计算缓存区域,OoO 机器和 SIMD 执行单元 (MMX + SSE1) 会占用大量区域,前端/解码器也是如此。
    • 预取器占据接近 50% 的芯片区域(更像是不到 1%)显然是没有意义的。早期的安腾处理器不包括硬件预取器,因为 VLIW 架构本身强调编译器优化。此外,与 x86 CPU 相比,Itanium 对软件预取有更好的支持(特别是隐式预取)。自 Poulson (2012) 以来的所有安腾处理器都包含硬件数据预取器。
    • 好的,我明白你为什么要连接它们,因为它们有利于相同的访问模式。但这是一个完全独立的微架构机制,它以不同的方式受益。就了解它们如何在幕后工作而言,它们是零联系的。
    • @PeterCordes:问题是我们如何定义 GPU 上的“核心”? NVIDIA 告诉我们 Titan Xp 有 3840 个 CUDA 内核。但这些内核与 CPU 内核不同。为了解释 GPU 的某些性能特征,我喜欢将 Titan Xp 视为只有 120 个物理内核,每个内核都在 128 字节向量寄存器上运行。事实上,我相信这就是它在硬件中的实现方式(虽然不确定)。 CUDA 只是给我们提供了拥有 3840 个内核的错觉,也就是说,这只是编程模型的一个属性。然后,内存合并是一种单(物理)核心优化。
    • @PeterCordes 为什么是幻觉?它们是物理的。由于限制(作为可用的寄存器,其中之一,如果不是最重要的),CUDA 不保证使用所有这些(参见占用)。因此,您有 120 个 SM,每个 SM 都有共同的加载/存储单元,能够在指令(通过扭曲)同时访问后续内存位置(而不仅仅是)时优化内存访问。我认为您将 SIMT 编程模型(这是一种抽象,具有比 3840 多得多的内核,在内核上调度线程的错觉)与物理硬件。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-06
    • 2012-05-06
    • 1970-01-01
    相关资源
    最近更新 更多