CUDA 具有“内置”(即预定义)向量类型,对于 4 字节数量(例如 int4)最大为 4,对于 8 字节数量最大为 2(例如 double2 )。一个 CUDA 线程的最大读/写事务大小为 16 字节,因此这些特定的大小选择往往与 that maximum 一致。
这些是作为典型结构公开的,因此您可以引用例如 .x 来仅访问向量类型的第一个元素。
与 OpenCL 不同,CUDA 不为基本算术提供内置操作(“重载”),例如+、- 等,用于对这些向量类型进行逐元素操作。没有什么特别的原因你不能自己提供这样的重载。同样,如果您想要uchar8,您可以轻松地为此类提供结构定义,以及任何所需的运算符重载。这些可能就像您对普通 C++ 代码所期望的那样实现。
那么,可能一个潜在的问题是,CUDA 和 OpenCL 在这方面的实现有什么区别?如果我在uchar8 上操作,例如
uchar8 v1 = {...};
uchar8 v2 = {...};
uchar8 r = v1 + v2;
OpenCL 和 CUDA 在机器性能(或低级代码生成)方面有何不同?
对于支持 CUDA 的 GPU 来说可能不多。 CUDA 核心(即底层 ALU)对 uchar8 上的此类操作没有直接的本机支持,此外,如果您编写自己的 C++ 兼容重载,您可能会为此使用 C++ 语义,这将本质上是串行的:
r.x = v1.x + v2.x;
r.y = v1.y + v2.y;
...
因此,这将分解为在 CUDA 内核(或在 CUDA SM 内的适当整数单元中)执行的一系列操作。由于 NVIDIA GPU 硬件不直接支持单核/时钟/指令中的 8 路 uchar 添加,因此 OpenCL(在 NVIDIA GPU 上实现)确实没有太大的不同。在底层,底层机器代码将是一系列操作,而不是一条指令。
顺便说一句,CUDA(或 PTX,或 CUDA 内在函数)确实在单个内核/线程/指令中提供了有限数量的向量操作。这方面的一些例子是:
一组有限的“本地”"video" SIMD instructions。这些指令是每个线程的,因此如果使用它们,它们允许每个 warp 最多支持 4x32 = 128(8 位)操作数,尽管操作数必须正确打包到 32 位寄存器中。您可以通过一组内置的intrinsics 直接从 C++ 访问这些。 (CUDA warp 是一组 32 个线程,是支持 CUDA 的 GPU 上锁步并行执行和调度的基本单元。)
向量 (SIMD) 乘法累加运算,不能直接转换为单个特定的元素运算重载,即所谓的 int8 dp2a 和 dp4a 指令。这里的 int8 有点误导。它不是指 int8 向量类型,而是指在单个 32 位字/寄存器中的 4 个 8 位整数数量的打包排列。同样,这些可以通过intrinsics 访问。
在 cc 5.3 及更高版本的 GPU 中,通过 half2 向量类型本机支持 16 位浮点,用于某些操作。
新的 Volta tensorCore 有点像单线程 SIMD 操作,但它在一组 16x16 输入矩阵上操作(warp-wide),产生 16x16 矩阵结果。
即使使用可以将某些向量操作映射到硬件“本机”支持的各种操作的智能 OpenCL 编译器,也不会完全覆盖。举一个例子,在单个核心/线程上,在单个指令中没有对 8 宽向量(例如uchar8)的操作支持。所以一些序列化是必要的。在实践中,我不认为 NVIDIA 的 OpenCL 编译器那么聪明,所以我的期望是,如果你研究机器代码,你会发现这种每线程向量操作完全序列化。
在 CUDA 中,您可以为某些 操作和向量类型提供自己的重载,这些重载可以近似地在一条指令中表示。例如,uchar4 add 可以使用__vadd4() intrinsic“本地”执行(可能包含在您的运算符重载实现中。)同样,如果您正在编写自己的运算符重载,我认为这不会很困难使用两条__vadd4() 指令执行uchar8 元素向量相加。