AMD GPU ISA 阅读

2024-06-09 14:28
文章标签 阅读 gpu amd isa

本文主要是介绍AMD GPU ISA 阅读,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Reading AMD GPU ISA — ROCm Blogs

对于应用开发者来说,了解用于执行其计算的 GPU 架构的指令集架构(ISA)通常是非常有帮助的。理解感兴趣的代码区域的指令可以帮助调试和实现应用程序的性能优化。

在这篇博客文章中,我们将讨论如何阅读和理解 AMD 的 Graphics Core Next (AMDGCN) 架构的 ISA,该架构被用于 AMD Instinct™ 和 AMD Radeon™ 系列 GPU 中。AMDGCN ISA 包含 AMDGCN 架构用于执行计算任务的指令。尽管我们将在本文中讨论几个重要的主题和示例,但我们鼓励读者参考相关的 ISA 文档以获取更多细节,例如,对于 AMD Instinct™ MI200™ GPU,参考 CDNA2™ ISA,或对于 AMD NAVI™ 20s GPU,参考 RDNA2™ ISA。我们将讨论以下内容:
• AMDGCN 架构概述。
• AMDGCN ISA 支持的指令和寄存器类型。
• 一些阅读和理解基本 ISA 指令的示例。

先决条件知识

在讨论其 ISA 时,理解以下的 AMDGCN 架构概念将是有帮助的。

术语解释

让我们定义本博客文章中常用的几个术语。
指令集架构(ISA):处理器(CPU、GPU 或 FPGA)的语言,定义了处理器能够执行的操作。ISA 在处理器的系统软件中定义的操作与这些操作在硬件上的执行映射之间充当接口。x86、ARM、RISC-V、GCN 都是特定于处理器的 ISA。
AMD Graphics Core Next (AMDGCN):AMD GPU 特有的 ISA。Southern Islands、Vega™、RDNA™(Radeon™)都是 AMDGCN ISA 的特定架构实现。

通常,ISA 是指特定处理器能够发出的所有可能指令。而“汇编”(ASM)指的是使用 ISA 的实际编程语言。然而,它们通常可以互换使用。

计算机架构知识点

这里讨论一些计算机架构的概念。
位(Bits)、字节(Bytes)和字(Words):一个字节是 8 位,一个字是 16 位(2 字节),一个双字(double word)是 32 位(4 字节)。假设以 C/C++ 作为程序实现语言并采用64位系统:一个 char 是 1 字节,一个 int 是 4 字节,一个 float 是 4 字节,一个 double 是 8 字节。在 AMDGCN ISA 中,经常可以看到 DWORD、`DWORDX2` 和 DWORDX4 指令。这些指示说明指令将分别对 4 字节(1 双字)、8 字节(2 双字)和 16 字节(4 双字)的数据进行操作。
指令类型:AMDGCN 实现为负载和存储架构,与 x86 的寄存器-存储器架构不同。因此,指令被分为两类:i) 存储操作(在存储器和寄存器之间的加载和存储),以及 ii) 算术逻辑单元(ALU)和浮点单元(FPU) 的操作(仅在寄存器之间发生)。

高位/低位比特:在指令集架构(ISA)中,最高有效比特(MSb)是一个数值中最重要的比特。高位比特是最左边的比特,而低位比特(最右边的比特)是数值中最不重要的比特(LSb)。

进位输入/输出:进位输入或输出指的是在算术运算中可能会溢出或下溢的比特。例如,如果两个32位数的算术运算产生超过32位的比特,那些比特就是“进位输出”,因为它们无法适应32位寄存器。这些比特会作为“进位输入”传递到表示更高位比特的32位寄存器中的下一个更重要的位置。因此,"进位输入/输出"操作便于使用较小精度的寄存器进行多精度算术运算。例如,即使是32位的寄存器也可以实现64位的运算。

执行掩码:执行掩码(`EXEC`)是一个决定在一个波前中哪些线程(threads)被执行的64位掩码:1 = 执行,0 = 不执行。所有向量指令都支持执行掩码。在GPU内核中,这个掩码通常用于处理流程分支的处理工作,其中每个分支只有一部分线程处于活跃状态。更多详细信息可以从 CDNA2™ ISA 的第3.3节中阅读到。 

在处理器的小单元之间

一个内核在 CDNA2™ ISA 中的一个波前由 64 个线程以锁步方式运行。在 RDNA2™ ISA (第二节). 中,波前的尺寸可以在 32 和 64 线程之间切换。处理器使用以下小单元来操作内核:
标量算术逻辑单元(SALU):SALU 对每个波前的一个值进行操作,这个值对所有线程都是共同的。所有内核流程都使用 SALU 指令处理,包括 if/else、分支和循环。
矢量算术逻辑单元(VALU):VALU 对每个线程的唯一值进行操作,但指令在一个波前的所有线程中一次通过地进行。每个波前都有一个 64 位的 EXEC 位掩码,标记每个线程为 1 (活动 - 处理指令)或 0(休眠 - 指令无操作)。

标量存储器(SMEM):SMEM 通过标量数据缓存在标量寄存器和内存之间传输数据。SMEM 指令在 SGPRs 和内存之间读写连续的 DWORDs。

矢量存储器(VMEM):VMEM 允许矢量寄存器与存储器之间的数据传输,每个线程都可以提供一个独特的存储器地址。VMEM 指令也支持 EXEC 掩码。
本地数据共享(LDS):AMD GPU 中的本地数据共享(LDS)是类似于 CUDA 中共享内存的高速、与寄存器相邻的存储空间。这个存储空间可以被看作是一个显式管理的缓存。

指令和寄存器类型

寄存器是位于芯片上处理单元附近的高速存储器。几乎所有的计算都使用寄存器。数据被 加载 到它们中,执行操作后数据被 存储 出来。

指令和寄存器分为标量和矢量两种形式。在 ISA 语言中,标量指令以 "s_" 开头,而矢量指令以 "v_" 开头。对于跨波前显然是统一的操作,使用标量指令。这里的统一是指波前中的每个线程使用相同的数据,即没有必要在线程间重复努力。对于编译器无法证明为统一的任何事务,使用矢量指令。最常见的示例是每个波前中的每个线程都使用来自内存中独特位置的数据。

标量指令只能在标量寄存器(SGPRs)中操作数据。矢量指令可以在矢量寄存器(VGPRs)中操作数据,但也可以读取存储在 SGPRs 中的数据。标量和矢量寄存器都是双字(32 位)大小,但可以连接起来以适应更大的数据类型。例如,一个单一的双精度浮点值或指针(64 位)将存储在两个连续的 32 位寄存器中。

在 MI200 系列 GPU 上,每个计算单元由一个 SALU 和四个 VALU 组成。每个 VALU 有 800 个 SGPRs,每个计算单元总共有 12.8 KB。一个波前中的单个线程可以使用多达 256 个 VGPRs 和 256 个累加 VGPRs(AGPRs),总共 2 KB。总之,一个计算单元有 524 KB 的 VGPRs 和 AGPRs。下面的图示以示意性的方式代表了 CU 内部的结构。这里 SIMD 是 VALUs。详见 ORNL 幻灯片 来获取细节。

常见指令

下面是一些在高性能计算应用程序中常见的指令:

算术指令

这些指令包括在 SALU 或 VALU 小单元上的整数算术指令。例如,`s_add_i32`、`s_sub_i32` 是使用 SALU 小单元完成的标量操作加法和减法。类似地,`v_add_i32`、`v_sub_i32` 是使用 VALU 小单元完成的相同操作。

移动

这类指令包括将输入移动到标量或矢量寄存器中。例如,`v_mov_b32` 将 32 位矢量输入移动到矢量寄存器。类似地,`s_mov_b64` 将 64 位标量输入移动到标量寄存器。

比较

在标量(SOPC)或矢量(VOPC)输入上执行比较操作的指令。这些指令的格式为 *_cmp_*。矢量比较指令在  个通道(或线程)上执行相同的比较操作。一个标量比较操作设置标量条件码(SCC),它被用作条件分支条件。矢量比较操作设置矢量条件码(VCC)掩码。

条件指令

条件指令基本上使用 SCC 值(0 或 1)来执行操作,或者决定使用哪个源操作数。例如,`s_cmove_b32` 将一个标量 32 位输入移动到一个标量寄存器。

加载/存储

加载和存储是主要类型的内存操作。这些操作从存储器中加载数据,并将执行算术操作后的数据从寄存器存储回存储器。例如,标量加载指令 (s_load_dword) 从存储器中加载一单倍字的数据到一个 SGPR 中。同样,矢量加载指令 global_load_dword 每个线程从 HBM 到矢量寄存器中加载了一个双倍字的数据。更多细节请参阅 CDNA2™ ISA。

指令与其与存储器的关系

以下是有关指令和存储器的一些重要点:

内存层次结构

虽然阅读 AMDGCN ISA 不一定需要对 GPU 的内存空间和层次结构有深入的理解,但了解 GPU 的内存空间和层次结构对帮助理解是有益的。例如,在 MI250X GPU 的单个图形计算芯片上,内存层次结构可以分解为:

图表注释:
1. 块是由多个波前组成的,然后波前由多个线程组成。波前内的线程可以向另一个线程的寄存器发出交叉通道指令。

2. 共享内存和 L1 缓存位于计算单元内,而 L2 缓存是在计算单元之间共享的。

作为旁注,要了解关于在 MI200 GPU 上高效使用内存空间的信息,请参考 MI200 GPU 内存空间博客文章。

划痕/堆栈内存

(Scratch memory 这个术语来源于其临时和快速访问的性质。就像在现实生活中,我们有时会“快速草拟”(scratch out a quick note)一个笔记来记住某事,scratch memory 在计算机中提供了临时的存储,以便快速记录下计算过程中产生的数据。然而,在计算机术语中,此术语的使用比这种类比更为技术化。

Scratch memory 的用法源自更早时期的计算机语言和系统,其中“scratchpad memory”常被用来指临时存储区,作为快速读写数据的地方,而非长期存储。在早期的计算机系统中,这种存崇空间经常被用作计算的一部分,但并不是程序的主要存储区域。

对于现代的 GPU 和 CPU,当寄存器(提供最快的数据存取速度)不足以存放所有临时变量时,scratch memory 提供了一个后备的存储选项。它仍然是线程私有的,这意味着每个线程都可以访问自己的 scratch memory 区域,但这部分内存位于全局内存中,因此访问速度比寄存器慢。

因此,术语“scratch”强调了这种存储类型的临时性和辅助性。尽管它更慢,但仍然是必要的,以保证即使在寄存器不足的情况下,每个线程也能持续进行运算。

Scratch memory" 翻译成中文通常可以说成“刮痕内存”或“即时内存”。这里的“刮痕”(Scratch)表达了该内存是用于临时快速存取的含义。不过,这个术语在中文中没有一个固定的标准翻译,有时会根据上下文进行适当的变通。例如,它也可以翻译为“暂存内存”、“快速临时内存”或者“辅助内存”。在实际的技术文档或者讨论中,专业人士可能会根据具体情况来选择最适合的翻译。在没有特定上下文的情况下,“暂存内存”可能是最直接传达其用途的翻译。)

如果内核中存在高寄存器压力,一些数据会被存储在一个特殊的内存空间,即属于全局内存但对线程私有的Scratch(划痕)内存中。这意味着数据访问速度比使用寄存器内存要慢。划痕内存不使用 LDS,因此划痕指令也不使用 LDS 带宽。这些指令只使用 vm_cnt(用于全局内存访问)而不使用 lgmk_cnt。以下示意图展示了线程访问划痕内存的方式。

示意图备注:
1. 线程和处理器之间的连接是通过波前实现的。实际上并不是每个处理器都和一个线程一一对应,而是每个四分之一波前的处理器会分为四个阶段来处理完整的波前。
2. 编译器寄存器使用和划痕诊断按线程报告。

ISA 示例

此部分通过几个简单的例子讨论 ISA 指令。可以使用编译器的 --save-temps 标志生成 ISA 源文件(包括 *.s 文件)。例如:

hipcc -c --save-temps -g example.cpp

虽然 --save-temps 足以生成相关的 ISA 源文件,但添加调试符号标志 -g 会进一步以相应的内核代码行注释 ISA。

要生成内核资源使用情况,例如 SGPR、VGPR、划痕内存、LDS 和占用率(每个 SIMD 的活动波前),请使用:

hipcc -c example.cpp -Rpass-analysis=kernel-resource-usage

请注意,这个报告只包含 编译时 信息。如果使用了运行时定义的动态共享内存或动态堆栈分配,则 -Rpass-analysis=kernel-resource-usage 不会报告正确的划痕内存、LDS 和/或占用率信息。

加载和存储

以下是几个代码示例及其对应的ISA:

简单的加载和存储

下面的代码片段显示了一个简单的HIP内核,包含一个加载和一个存储操作:

__global__
void load_store(int n, float* in, float* out)
{int tid = threadIdx.x + blockDim.x * blockIdx.x;out[tid] = in[tid];
}

这个内核的注释版ISA为:

; %bb.0:s_load_dword s7, s[4:5], 0x24          # SGPRs s[4:5] 保存内核参数# 和内核分派包。# 从s[4:5]的偏移量36 (=0x24)读取# 并保存blockDim.x到s7。s_load_dwordx4 s[0:3], s[4:5], 0x8     # 保存输入数组in[]到SGPR对s[0:1],输出数组out[]到SGPR对s[2:3]。s_waitcnt lgkmcnt(0)                   # 等待标量内存加载完成,直到计数器(lgkmcnt)值减少到0。s_and_b32 s4, s7, 0xffff               # 保留blockDim.x (s7)的低位比特,并将所有高位比特置为0。保存到s4。s_mul_i32 s6, s6, s4                   # s6=blockDim.x * blockIdx.x (s4 * s6)v_add_u32_e32 v0, s6, v0               # tid (v0) = threadIdx.x (v0) + s6# --- 输入数组in[]和输出数组out[]的整数操作 ---v_ashrrev_i32_e32 v1, 31, v0           # 将32位的v0存储到64位对v[0:1]中v_lshlrev_b64 v[0:1], 2, v[0:1]        # tid<<2 (tid *= 4),第tid个元素的偏移量是tid*4字节从第0个元素开始v_mov_b32_e32 v3, s1                   # 将in[]的高位地址(s1)移动到VGPR v3的高位比特上v_add_co_u32_e32 v2, vcc, s0, v0       # 将in[]的基地址(s0)加上tid*4(v0)得到# 每个tid的地址。v_addc_co_u32_e32 v3, vcc, v3, v1, vcc # 将进位加到tid*4的高位比特上,保存在v3# 现在,VGPR对v[2:3]拥有了in[]的正确地址,对于每个线程global_load_dword v2, v[2:3], off      # 从v[2:3]加载in[]到v2。v_mov_b32_e32 v3, s3                   # 当v2数据被加载并准备使用的时候,让我们进行一些out[]的指针运算v_add_co_u32_e32 v0, vcc, s2, v0       # 类似于上面的数组输入操作v_addc_co_u32_e32 v1, vcc, v3, v1, vcc # v[0:1] = 每个线程正确的out[]地址s_waitcnt vmcnt(0)                     # 等待"全局内存"计数器vmcnt减少到值0。# 它等待global_load_dword操作执行完毕,# 再进行存储操作。global_store_dword v[0:1], v2, off     # 在v[0:1]中存储全局数据v2s_endpgm                               # 内核程序结束前隐式等待全局存储操作完成。# 该指令告诉硬件波前已完成。

请注意,AMDGCN调用约定的明确要求是,内核参数必须通过SGPRs传递。这与通过将函数参数推入栈来传递的x86不同。

带条件的加载和存储

让我们在上面的内核中引入一个条件,确保线程的内存访问保持在数组范围内。以下代码片段显示了更新后的内核。

__global__
void load_store(int n, float* in, float* out)
{int tid = threadIdx.x + blockDim.x * blockIdx.x;if (n > tid)out[tid] = in[tid];
}

上述带有条件语句的加载和存储HIP内核的ISA如下:

; %bb.0:# --- 第1部分: 见简单内核 ---s_load_dword s0, s[4:5], 0x24s_load_dword s1, s[4:5], 0x0s_waitcnt lgkmcnt(0)s_and_b32 s0, s0, 0xffffs_mul_i32 s6, s6, s0v_add_u32_e32 v0, s6, v0                 # 保存每个线程的tid到v0。# --- 第2部分: 条件 ---v_cmp_gt_i32_e32 vcc, s1, v0             # 64位寄存器对向量条件码# 'vcc'持有布尔值(0或1) "n (s1) > tid (v0)"s_and_saveexec_b64 s[0:1], vcc           # 64位寄存器对s[0:1]存储# 执行掩码 'exec' 指示激活/掩蔽(1/0)的通道数# SGPR寄存器对(exec掩码)# 存储对于"tid < n"为真的所有tid为1,否则为0.s_cbranch_execz .LBB0_2                  # 如果所有执行掩码都是# 0位,跳转到程序的末尾。# 见LBB0_2下面。# --- 第3部分: 全局加载/存储 ---# 整数运算和全局加载/存储指令# 与简单内核相同。# 注意:第3部分只# 为s[0:1]位值为1的线程执行。
; %bb.1:s_load_dwordx4 s[0:3], s[4:5], 0x8       # s[0:1]: in[], s[2:3]: out[]v_ashrrev_i32_e32 v1, 31, v0             # ...v_lshlrev_b64 v[0:1], 2, v[0:1]s_waitcnt lgkmcnt(0)v_mov_b32_e32 v3, s1v_add_co_u32_e32 v2, vcc, s0, v0v_addc_co_u32_e32 v3, vcc, v3, v1, vccglobal_load_dword v2, v[2:3], off        # 将全局数据in[tid]加载到v2v_mov_b32_e32 v3, s3                     # 指针运算,用于out[]v_add_co_u32_e32 v0, vcc, s2, v0         # ...v_addc_co_u32_e32 v1, vcc, v3, v1, vcc   # ...s_waitcnt vmcnt(0)                       # 等待全局加载完成# 并且在VGPR v2中可用。global_store_dword v[0:1], v2, off       # 将加载的全局数据v2# 存储到VGPR对v[0:1]中。
.LBB0_2:s_endpgm                                 # 在程序结束前隐式地等待直到全局存储# 完成。波前完成。

划分为暂存存储空间

内核中使用暂存空间的一个例子可以通过简单地在内核中分配一个数组来完成,其大小超过了寄存器所能承受。例如:

__global__ void kernel(int* x, int len)
{int y[16] = {0}; //64 bytesint i = blockDim.x * blockIdx.x + threadIdx.x;if (i < len) {x[i] = y[i];}
}

上述内核的资源使用情况:

SGPRs: 11 [-Rpass-analysis=kernel-resource-usage]
VGPRs: 3 [-Rpass-analysis=kernel-resource-usage]
AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
ScratchSize [bytes/lane]: 0 [-Rpass-analysis=kernel-resource-usage]
Occupancy [waves/SIMD]: 8 [-Rpass-analysis=kernel-resource-usage]
SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

这里的 y 可以放在向量寄存器(VGPRs)中。然而,如果寄存器数组大小进一步增加:

__global__ void kernel(int* x, int len)
{int y[17] = {0}; //68 bytesint i = blockDim.x * blockIdx.x + threadIdx.x;if (i < len) {x[i] = y[i];}
}

这个内核的资源使用情况现在如下:

SGPRs: 14 [-Rpass-analysis=kernel-resource-usage]
VGPRs: 4 [-Rpass-analysis=kernel-resource-usage]
AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
ScratchSize [bytes/lane]: 96 [-Rpass-analysis=kernel-resource-usage]
Occupancy [waves/SIMD]: 8 [-Rpass-analysis=kernel-resource-usage]
SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

y 不再放在向量寄存器中,因为它太大了,现在被溢出到暂存内存中。机器上的物理内存并非无限细腻,我们可以看到栈是以96字节的“块”来分配的。以下ISA代码显示,通过使用`buffer_store_dword`指令将scratch内存存储在17个缓冲栈中:

buffer_store_dword v1, off, s[0:3], 0 offset:76
buffer_store_dword v1, off, s[0:3], 0 offset:72
buffer_store_dword v1, off, s[0:3], 0 offset:68
buffer_store_dword v1, off, s[0:3], 0 offset:64
buffer_store_dword v1, off, s[0:3], 0 offset:60
buffer_store_dword v1, off, s[0:3], 0 offset:56
buffer_store_dword v1, off, s[0:3], 0 offset:52
buffer_store_dword v1, off, s[0:3], 0 offset:48
buffer_store_dword v1, off, s[0:3], 0 offset:44
buffer_store_dword v1, off, s[0:3], 0 offset:40
buffer_store_dword v1, off, s[0:3], 0 offset:36
buffer_store_dword v1, off, s[0:3], 0 offset:32
buffer_store_dword v1, off, s[0:3], 0 offset:28
...
buffer_store_dword v1, off, s[0:3], 0 offset:24
buffer_store_dword v1, off, s[0:3], 0 offset:20
buffer_store_dword v1, off, s[0:3], 0 offset:16
buffer_store_dword v1, off, s[0:3], 0 offset:80

请注意,寄存器溢出到scratch内存的情况严重依赖于GPU架构和ROCm™版本。

位移拷贝

位移拷贝的一个示例是:

__global__ void shifted_copy (float *in, float *out) {size_t gid = blockDim.x * blockIdx.x + threadIdx.xout[gid] = in[gid+4];
}

以下是上述 shifted_copy 内核的显著的ISA指令:

s_load_dwordx4 s[0:3], s[4:5], 0x0          # s[0:1] 64位: 输入数组 in[], s[2:3] 64位: 输出数组 out[]# 从内核参数 s[4:5] 读取
v_lshlrev_b64 v[0:1], 2, v[0:1]             # gid 存储在 v[0:1] 64位中。# v[0:1] 左移2位,来# 账户每个浮点元素的 in[] 4字节的访问。# 例如:gid[波段=0] 在 0 和# gid[波段=1] 在 4v_add_co_u32_e32 v2, vcc, s0, v0            # 向每个线程的虚拟寄存器 v[0:1] # 添加 in[] 的基地址# 存储在 s[0:1] 以访问 corr 地址 [gid],# 并保存在 v[2:3]global_load_dword v2, v[2:3], off offset:16 # 全局加载 float,地址偏移# 16=(4移位)*(4字节/float),# 保存在 v2 64位
v_add_co_u32_e32 v0, vcc, s2, v0            # int 操作通过每个线程访问 out[] 指针
v_addc_co_u32_e32 v1, vcc, v3, v1, vcc      # 在 [gid],并在 v[0:1] 中存储s_waitcnt vmcnt(0)                          # 波在 vmem 指令前等待# 直到所有加载完成,# 或直到加载等待计数器# 达到 0。即,等待直到# v2 中的数据加载完成并在波中可用
global_store_dword v[0:1], v2, off          # 加载完成后,将# 数据存储在数组 out[] 的 v[0:1] 中

请注意,通过4个索引的偏移复制体现在指令中:`global_load_dword v2, v[2:3], off offset:16`。这里的16指的是每个浮点数4字节乘以总共4次偏移。如果我们使用双精度浮点型,相应的指令将是:`global_load_dword2 v2, v[2:3], off offset:32`。这里的总偏移是每个双精度8字节乘以总共4次偏移,即32。同样注意到 dwordx2 用于加载双精度类型。

Pragma unroll

编译器指令 #pragma unroll <unroll_factor> 可以通过控制 <unroll_factor> 来优化内核性能。较大的 unroll_factor 可能会yield更低的执行时间,但可能会导致更大的寄存器压力和减少占用。例如,让我们比较 unroll 因子为8和32的循环在 unroll 内核示例中的第一个循环。

Baseline kernel (unroll factor=8)

__global__ void kernel_unroll(float* in, size_t fac)
{size_t tid = threadIdx.x;if (tid >= N)return;float temp[NITER];#pragma unroll 8for (size_t it = 0; it < NITER; ++it)temp[it] = in[tid + it*fac];for (size_t it = 0; it < NITER; ++it)if (temp[it] < 0.0)in[tid + it*fac] = 0.0;
}

Optimized kernel (unroll factor=32)

__global__ void kernel_unroll(float* in, size_t fac)
{size_t tid = threadIdx.x;if (tid >= N)return;float temp[NITER];#pragma unroll 32for (size_t it = 0; it < NITER; ++it)temp[it] = in[tid + it*fac];for (size_t it = 0; it < NITER; ++it)if (temp[it] < 0.0)in[tid + it*fac] = 0.0;
}

以下是常见于这些内核的一些变量的值:

#define N 1024 * 1024 * 8
#define NITER 128

基准内核(unroll 因子为8)的内核使用情况如下所示:

SGPRs: 22 [-Rpass-analysis=kernel-resource-usage]
VGPRs: 21 [-Rpass-analysis=kernel-resource-usage]
AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
ScratchSize [bytes/lane]: 528 [-Rpass-analysis=kernel-resource-usage]
Occupancy [waves/SIMD]: 8 [-Rpass-analysis=kernel-resource-usage]
SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

使用32的展开因子,我们注意到VGPRs的数量大约增加了两倍。

SGPRs: 22 [-Rpass-analysis=kernel-resource-usage]
VGPRs: 42 [-Rpass-analysis=kernel-resource-usage]
AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
ScratchSize [bytes/lane]: 528 [-Rpass-analysis=kernel-resource-usage]
Occupancy [waves/SIMD]: 8 [-Rpass-analysis=kernel-resource-usage]
SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

在查看基线代码的ISA源代码 *.s 时,最大的 global_load_dword VGPR索引是v20,这对应于21个VGPRs。

...
global_load_dword v20, v[8:9], off
...

另一方面,具有32大小的pragma unroll的内核具有最大VGPR索引为v41,相当于42个VGPRs。

...
global_load_dword v41, v[10:11], off
...

请注意,使用大小为32的pragma unroll时,ISA将只显示32个全局加载。然而,因为循环有 niter = 128,所以总共必须有128个全局加载。使用pragma unroll的内核简单地通过4次这样的传递来执行128个全局加载,每个传递中有32个全局加载。

继续讨论循环展开,我们必须小心使用过大的pragma unroll大小。例如,展开大小为64的内核会导致更大的寄存器(VGPRs)使用和减少的占用率,相比于展开大小为32,占用率减少到每SIMD 6个波浪。这从其内核使用摘要中可以清晰看到:

SGPRs: 22 [-Rpass-analysis=kernel-resource-usage]
VGPRs: 74 [-Rpass-analysis=kernel-resource-usage]
AGPRs: 0 [-Rpass-analysis=kernel-resource-usage]
ScratchSize [bytes/lane]: 528 [-Rpass-analysis=kernel-resource-usage]
Occupancy [waves/SIMD]: 6 [-Rpass-analysis=kernel-resource-usage]
SGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
VGPRs Spill: 0 [-Rpass-analysis=kernel-resource-usage]
LDS Size [bytes/block]: 0 [-Rpass-analysis=kernel-resource-usage]

最大的 VPGR 索引的 global load 从 ISA 源文件观察到是 v73,对应 74 VPGRs:

...
global_load_dword v73, v[10:11], off
...
警告
 有时编译器可能会默认使用循环展开进行优化。这可能会导致较大的寄存器使用并可能降低占用率。在上述例子中,由于编译器优化,如果没有包含任何 pragma unroll 指令,则仍然会导致 pragma unroll 因子为128,例如在MI250上的rocm/6.1.0。这将导致更大的寄存量使用85 VGPRs和更低的占用率,每SIMD 5波。

请注意,上面的例子中有较大的划痕分配(528字节/线程)。这并不奇怪,因为内核使用了一个大的堆栈数组 temp[NITER]。这在之前关于划痕的例子中讨论过。理想情况下,在内核中应该避免使用如此大的堆栈分配,以进一步提高其性能。这也在 Register pressure in AMD CDNA™2 GPUs — ROCm Blogs 博客帖子中进行了讨论。

总结

在这篇博客中,我们讨论了如何阅读 AMDGCN 架构的 ISA。我们讨论了一些基本的指令类型,它们与处理器子单元和内存层次结构的关系。为了让读者熟悉 ISA,我们使用了一些例子。虽然这作为对 AMDGCN ISA 阅读的良好介绍,但鼓励读者参考特定的 AMDGCN ISA 文档。如果您有任何问题或评论,请在 GitHub 讨论区与我们联系 Discussions

额外资源

• AMDGCN Assembly
• LLVM-AMDGPU-Assembler-Extra
• Assembly cross lane operations
• AMD ISA Doc
• RDNA3™ ISA guide 

这篇关于AMD GPU ISA 阅读的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1045433

相关文章

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

AI Toolkit + H100 GPU,一小时内微调最新热门文生图模型 FLUX

上个月,FLUX 席卷了互联网,这并非没有原因。他们声称优于 DALLE 3、Ideogram 和 Stable Diffusion 3 等模型,而这一点已被证明是有依据的。随着越来越多的流行图像生成工具(如 Stable Diffusion Web UI Forge 和 ComyUI)开始支持这些模型,FLUX 在 Stable Diffusion 领域的扩展将会持续下去。 自 FLU

如何用GPU算力卡P100玩黑神话悟空?

精力有限,只记录关键信息,希望未来能够有助于其他人。 文章目录 综述背景评估游戏性能需求显卡需求CPU和内存系统需求主机需求显式需求 实操硬件安装安装操作系统Win11安装驱动修改注册表选择程序使用什么GPU 安装黑神话悟空其他 综述 用P100 + PCIe Gen3.0 + Dell720服务器(32C64G),运行黑神话悟空画质中等流畅运行。 背景 假设有一张P100-

论文阅读笔记: Segment Anything

文章目录 Segment Anything摘要引言任务模型数据引擎数据集负责任的人工智能 Segment Anything Model图像编码器提示编码器mask解码器解决歧义损失和训练 Segment Anything 论文地址: https://arxiv.org/abs/2304.02643 代码地址:https://github.com/facebookresear

软件架构模式:5 分钟阅读

原文: https://orkhanscience.medium.com/software-architecture-patterns-5-mins-read-e9e3c8eb47d2 软件架构模式:5 分钟阅读 当有人潜入软件工程世界时,有一天他需要学习软件架构模式的基础知识。当我刚接触编码时,我不知道从哪里获得简要介绍现有架构模式的资源,这样它就不会太详细和混乱,而是非常抽象和易

GPU 计算 CMPS224 2021 学习笔记 02

并行类型 (1)任务并行 (2)数据并行 CPU & GPU CPU和GPU拥有相互独立的内存空间,需要在两者之间相互传输数据。 (1)分配GPU内存 (2)将CPU上的数据复制到GPU上 (3)在GPU上对数据进行计算操作 (4)将计算结果从GPU复制到CPU上 (5)释放GPU内存 CUDA内存管理API (1)分配内存 cudaErro

【阅读文献】一个使用大语言模型的端到端语音概要

摘要 ssum框架(Speech Summarization)为了 从说话人的语音提出对应的文本二题出。 ssum面临的挑战: 控制长语音的输入捕捉 the intricate cross-mdoel mapping 在长语音输入和短文本之间。 ssum端到端模型框架 使用 Q-Former 作为 语音和文本的中介连接 ,并且使用LLMs去从语音特征正确地产生文本。 采取 multi-st

PyInstaller问题解决 onnxruntime-gpu 使用GPU和CUDA加速模型推理

前言 在模型推理时,需要使用GPU加速,相关的CUDA和CUDNN安装好后,通过onnxruntime-gpu实现。 直接运行python程序是正常使用GPU的,如果使用PyInstaller将.py文件打包为.exe,发现只能使用CPU推理了。 本文分析这个问题和提供解决方案,供大家参考。 问题分析——找不到ONNX Runtime GPU 动态库 首先直接运行python程序

麒麟系统安装GPU驱动

1.nvidia 1.1显卡驱动 本机显卡型号:nvidia rtx 3090 1.1.1下载驱动 打开 https://www.nvidia.cn/geforce/drivers/ 也可以直接使用下面这个地址下载 https://www.nvidia.com/download/driverResults.aspx/205464/en-us/ 1.1.3安装驱动 右击,

你读文献的方式可能错了!掌握这些技巧,让阅读事半功倍!

我是娜姐 @迪娜学姐 ,一个SCI医学期刊编辑,探索用AI工具提效论文写作和发表。 科研新手如何精读一篇论文? 很多科研新手,一上来就疯狂下载几十上百篇文献。囫囵吞枣看完了,还是什么都不知道,大脑一片空白。究竟该如何读文献收获最大? 大佬说,要积极阅读、频繁阅读。 什么是积极阅读? 相比被动阅读,积极阅读是指在阅读之前准备好问题、设置阅读目标、保持批判性,收获更多、进步更大的一种阅读