使用CUDA向量类型有什么优势?

17

CUDA提供了像uint2uint4等内置向量数据类型。使用这些数据类型有什么优点?

假设我有一个由两个值A和B组成的元组。一种将它们存储在内存中的方法是分配两个数组。第一个数组存储所有A值,第二个数组以对应于A值的索引存储所有B值。另一种方法是分配一个uint2类型的数组。哪种方式应该使用?哪种方式被推荐?uint3的成员,即xyz是否在内存中相邻地存在?


似乎很奇怪的是,CUDA没有提供内置的向量操作,因为每个着色语言都有,并且您知道底层硬件支持它。我只看到CUDA API在纹理读取中使用它们。这是我对CUDA最大的谜团。 - wcochran
@wcochran:CUDA中的每个操作都是矢量操作:warp中的32个线程中的每一个对应于矢量中的一个插槽。有足够的硬件/软件支持,以在多线程模型中呈现给您。 - user1084944
点、叉、加、减、缩放、反射等在哪里? - wcochran
3个回答

7

这可能有点推测性,但可能会补充@ArchaeaSoftware的答案。

我主要熟悉Compute Capability 2.0(Fermi)。对于这种架构,我认为使用向量化类型没有任何性能优势,除非是8位和16位类型。

查看char4的声明:

struct __device_builtin__ __align__(4) char4
{
    signed char x, y, z, w;
};

这个类型对齐到4个字节。我不知道__device_builtin__的作用。也许它会在编译器中触发一些魔法...

float1float2float3float4的声明看起来有点奇怪:

struct __device_builtin__ float1
{
    float x;
};

__cuda_builtin_vector_align8(float2, float x; float y;);

struct __device_builtin__ float3
{
    float x, y, z;
};

struct __device_builtin__ __builtin_align__(16) float4
{
    float x, y, z, w;
};
float2会得到某种特殊处理。float3是一个没有任何对齐的结构体,而float4则会被对齐到16字节。我不确定这意味着什么。
全局内存事务是128字节,对齐到128字节。事务总是一次性为整个warp执行。当一个warp到达执行内存事务的函数时,例如从全局内存加载32位数据,芯片将在此时执行尽可能多的事务以服务于warp中的所有32个线程。因此,如果所有访问的32位值都在单个128字节行内,则只需要一次事务。如果值来自不同的128字节行,则会执行多个128字节事务。对于每个事务,warp将暂停大约600个周期,同时从内存中获取数据(除非它在L1或L2缓存中)。
因此,我认为找出哪种方法能够提供最佳性能的关键是考虑哪种方法会导致最少的128字节内存事务。
假设内置向量类型只是结构体,其中一些具有特殊对齐方式,使用向量类型会使值以交错方式存储在内存中(结构体数组)。因此,如果warp在该点加载所有x值,则由于128字节事务,其他值(yzw)将被拉入L1。当warp稍后尝试访问它们时,它们可能不再在L1中,因此必须发出新的全局内存事务。此外,如果编译器能够发出更宽的指令以同时读取更多的值,以备将来使用,它将使用寄存器来存储这些值在加载点和使用点之间,从而可能增加内核的寄存器使用量。
另一方面,如果将值打包到数组的结构体中,则可以用尽可能少的事务来服务于加载。因此,当从x数组中读取时,只会加载x值在128字节事务中。这可能导致较少的事务、对缓存的较少依赖以及计算和内存操作之间更均匀的分布。

2
所以,我认为找出哪种方法能够提供最佳性能的关键是考虑哪种方法会导致最少的128字节内存事务。但并非总是如此。您应该观看Paulius的演示之一,例如http://bit.ly/OzutxO。增加同时进行的事务通常有助于提高带宽利用率。 - harrism
所以,如果warp在那个点加载了所有的x值,由于128字节的事务,其他值(y、z、w)将被拉入L1缓存。当warp稍后尝试访问这些值时,它们可能已经不在L1缓存中了。如果你从一个float4数组加载到一个float4变量中(该变量将存储在寄存器中),你就不必担心当线程使用它们时,y、z和w是否在缓存中,因为线程会在寄存器中拥有它们。对于需要float4数据(或符合其他结构体条件)的应用程序,通常是的,你应该使用结构体。 - harrism
@harrism 在上述 float4 结构中,__builtin_align__(16) 的确切作用是什么? - username_4567
1
它告诉编译器在16字节对齐的边界上分配结构体(或这些结构体的数组)。我相信__builtin_align____align__的包装器。您可以通过查看CUDA头文件来确认。__align__应该在CUDA C编程指南中有描述。 - harrism
@harrism:感谢您指向这些演示文稿。我参加了GTC,但不认为我看到了Paulius的任何演示文稿。我会尝试去看一下。在加载矢量类型后不得不依赖缓存,我不知道当时在想什么...我可以删除我的答案,但是您的评论也会消失。您想添加一个包含这些评论的答案吗? - Roger Dahl
不,我认为在你的答案、Nick的答案和评论之间,已经很好地涵盖了。 - harrism

5
我不相信CUDA内置的元组([u]int[2|4], float[2|4], double[2])有任何固有优势;它们主要是为了方便而存在。您可以定义自己的C++类,具有相同的布局,编译器会有效地操作它们。硬件确实具有本机64位和128位负载,因此您需要检查生成的微码以确保。

至于是否应该使用uint2数组(结构体数组或AoS)还是两个uint数组(数组的结构体或SoA),没有简单的答案——这取决于应用程序。对于方便大小的内置类型(2x32位或4x32位),AoS具有优势,因为您只需要一个指针来加载/存储每个数据元素。SoA需要多个基指针,或者至少需要多个偏移量和单独的加载/存储操作才能处理每个元素;但对于有时仅操作子集元素的工作负载可能更快。

作为使用AoS的好例子,请查看nbody示例(它使用float4来保存每个粒子的XYZ+质量)。Black-Scholes示例使用SoA,可能是因为float3是一种不方便的元素大小。


6
硬件具有64位和128位的加载和存储。通常,像uint2uint4这样的结构(如果它们适用于您的数据和算法)是有优势的,因为它们可以增加每个线程的事务大小,从而更有效地利用可用带宽。您可以创建自己的自定义结构,但要确保它们像CUDA提供的结构一样指定对齐方式。 - harrism
@harrism 所以如果我没错的话,应该是这样的... uint2 的所有成员都会在内存中相邻地存在,因此使用 uint2 类型的数组可以减少内存事务,因为一个操作可以同时处理两个值。 - username_4567
1
是的。请查看CUDA SDK中nbody、particles和其他物理演示中如何使用float4。 - harrism
+1 我知道这是一个旧帖子,但我认为你的第二和第三段非常重要。通常在并行计算中,我经常听到SoA更好,但正如nbody示例代码所证明的那样,并非总是如此。 - James

3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接