我刚刚向 Futhark 添加了半精度浮点数,如 f16 类型。这并不是特别困难(也许是一天的工作),但由于相当浅薄的技术原因,这很烦人,所以作为一点宣泄,这里有一篇关于我遇到的挑战的博客文章。首先,为什么我们甚至想要 16 位浮点数?它们的精度非常糟糕,最大的可表示值仅为 65504。显然 16 位浮点数在 80 年代初被各种奇特的处理器使用,但最终由 IEEE 754-2008 标准化的现代谱系来自图形编程。这是因为半精度浮点数可用于表示光强度,因为它们足够准确,同时允许比 16 位定点数大得多的动态范围。最初,半精度仅用作存储格式,但最终 GPU 支持直接对半精度浮点数进行操作;通常具有两倍的算术吞吐量。由于它们只占用单精度浮点数一半的内存空间(因此也使用一半的内存带宽),因此理论上它们的速度是单精度浮点数的“两倍”。半精度浮点数在机器学习应用中也越来越流行,因为神经网络似乎对数值问题有抵抗力(大概它们只是围绕它们进行训练)。但这就是事情变得有趣的地方:实际上(至少)有两种半精度浮点格式。所有这些都在内存中占用 16 位,但不同之处在于它们如何将这些位分配给有效数和指数。我们在 Futhark 中支持的是 IEEE 754-2008 中的 binary16 格式,但谷歌也开发了 bfloat16 用于 AI 加速器。 ARM 显然也支持 binary16 的非标准变体,但我希望我永远不必了解细节。所以我们想要支持半精度浮点数,因为一些应用程序可以使用它们,或者因为它们占用更少的空间或者因为它们的计算实际上更快。半精度浮点数已在 IEEE 754-2008 中标准化,但它们并不是通用编程语言中普遍支持的类型。特别是,ISO C 不支持它们。其他语言有一些不寻常的限制,即支持一半作为“存储类型”(意味着您可以将它们放在内存中),但是如果您真的想对它们进行算术运算,则需要将它们转换为其他类型(例如浮点数)。这不是我们想要在 Futhark 中做到的。 f16 类型应该像任何其他原始类型一样在所有系统上得到完全支持。必要时,编译器必须生成代码以弥补缺乏硬件支持。 Futhark 的大多数后端都生成 C 代码 - 无论是普通的 CPU C、OpenCL C 还是 CUDA C。让我们从不支持半类型的普通 C 开始。一种选择是将 f16 表示为 16 位整数,然后手动实现每个算术运算。人们已经这样做了,但是这样的代码运行非常缓慢。相反,我们只是简单地 typedef float f16 并实际以单精度执行 f16 操作。但是,我们不希望这些模拟的浮点数在内存中每个元素占用 32 位,因此我们区分了用于操作的标量类型浮点数和用于数组的存储类型 uint16_t。当从数组中读取 f16 时,我们必须使用一个函数将存储类型转换为标量类型——幸运的是,上面链接的库包含这样一个函数及其逆函数。这意味着 f16 操作可能会产生不同的结果,具体取决于它们是否被仿真,因为仿真只在最后执行舍入,而真正的 f16 计算也可能对中间结果进行舍入。我对数值分析或 IEEE 754 的保证没有足够的经验,无法说明这是否是一个真正的问题。 Futhark 的 C API 也有问题。通常,返回标量的入口点将作为 C 函数公开,该函数产生明显类型的值(int32_t 用于 i32 等),但 C 没有半类型。我们再次回到“存储类型”的概念并将 f16 结果作为 uint16_t 返回。如果一半得到普遍支持,生活就会容易得多。
让我们继续讨论 OpenCL 后端。当然,那里的生活更轻松!不,从来都不是。 OpenCL 实际上定义了一个 half 类型,但只是作为一种存储格式,这意味着您可以拥有指向它们的指针,但不能执行任何算术运算,除非相关平台支持 cl_khr_fp16 扩展(并且您启用它)。 OpenCL 平台是动态加载的,然后在内核中进行运行时编译,因此 Futhark 编译器在编译时不知道目标平台是否支持半精度。因此,我们使用大量#ifdef 生成内核代码以检测平台是否支持 cl_khr_fp16,否则返回到基于浮点的仿真。值得注意的是,OpenCL 确实提供了内置函数,可以在存储在内存中的单精度和半精度浮点数之间进行有效转换,即使对于那些没有 cl_khr_fp16 的平台也是如此。这让我们能够以相当高的速度将半精度浮点数加载到单精度标量中。我注意到与手写转换函数相比有一个数量级的差异。这非常重要,因为出于某种原因,NVIDIA 的 OpenCL 实现不支持 cl_khr_fp16,尽管他们的 GPU 完全有能力。作为一个令人愉快的怪癖,OpenCL 模拟器 Oclgrind 声称支持 cl_khr_fp16,但实际上无法对半值执行任何操作。 Futhark 用户永远不会使用 Oclgrind,但它绝对是破解编译器不可或缺的工具,也用于我们的回归测试套件。解决方案是临时检查以始终在 Oclgrind 上模拟半精度。好的,CUDA 时间到了。 NVIDIA 一直在努力推销半精度,所以肯定一切都会好起来的,对吧?因此,CUDA 确实支持 Compute Capability 6.0 或更高版本的设备上的半精度浮点数。这可以用#ifdef 来检查。但是,出于某种奇怪的原因,您必须包含一个特殊的头文件 cuda_fp16.h,才能真正访问 half 类型及其操作。看一眼头文件,它看起来像 half 实际上是作为带有内联 PTX(CUDA 程序集)的 C++ 类实现的,以使用本机支持的半精度指令执行计算。这是非常巴洛克风格的。为什么不像在 OpenCL 中那样在编译器中实现 half 类型?我怀疑原因是 CUDA 主要是“单源”编程模型,其中同一程序同时包含 CPU 和 GPU 代码,其中 CPU 代码最终由系统编译器编译——它不支持半精度浮点数。相比之下,OpenCL C 专门用于“设备”代码(阅读:GPU 代码),而“主机”(CPU)则完全超出其权限范围。 Futhark 并没有以“单一源”的方式使用 CUDA,而是使用 NVRTC 来对 CUDA 内核进行运行时编译,这与 OpenCL 的工作方式非常相似。包含 cuda_fp16.h 的需要不仅仅是编译器实现的好奇心 - 事实证明,在整个偶然复杂性的行列中,它带来了最大的烦恼。显然,当您使用 NVRTC 运行时编译 CUDA 内核时,默认搜索路径不包括 CUDA 包含目录,因此找不到此头文件。调用 NVRTC API 的人必须明确提供包含此标头的目录。这是不好的。长期以来,我一直试图确保 Futhark 不会导致日益严重的意外复杂性灾难,因为我主要厌恶新的配置文件和环境变量。但现在看来,我们别无选择。 Futhark 必须能够在运行时找出 CUDA 的安装位置,并且没有标准。当然,它通常在 /usr/local/cuda 中,但将它放在 /opt 中的某个位置也很常见,尤其是在超级计算机系统上。并且没有指向CUDA目录的标准环境变量!其他 GPU 应用程序尊重任何或全部或 CUDA_HOME、CUDA_PATH 和 CUDA_ROOT。而且,令我非常遗憾的是,现在 Futhark 也是如此。
这是一个悲剧和耻辱。没有充分的理由必须这样。我知道这会导致事情无法正常工作,只是因为用户没有正确设置不可见的全局变量。我仍然希望我只是在某处错过了一个微妙的细节,而这最终会像一场噩梦一样消失。除速度外,半精度浮点数是 IEEE 最差的浮点数。那么他们在 Futhark 中更快吗?有点。在 A100 GPU 上,表达式 f16.sum 可以在 2.1 毫秒内求和 10 亿个半精度浮点数,而 f32.sum 需要 2.9 毫秒来求和 10 亿个单精度浮点数。这是 1.38 倍的加速,远低于我们理论上预期的 2 倍。那么出了什么问题呢?两件事:单独添加两个半精度浮点数可能并不比添加两个单精度浮点数快。 CUDA 的半精度 API 强烈暗示应该考虑使用 half2 类型,它将两个半精度数字打包成 32 位,并允许在单个指令中对两者进行“向量化”操作。据我了解当前的硬件,这是半精度浮点数可以实现高算术吞吐量的方式。我不知道 CUDA 编译器是否会自己将多个 halfs 优化为 half2s,但我对此表示怀疑 - 特别是对于减少。上面的分析是有效的,但实际上并不是这里发生的事情。这个总和完全受带宽限制,所以唯一重要的是我们访问内存的效率。在 f16.sum 中,每个线程一次读取一个 16 位值。虽然 Futhark 编译器会安排一些事情以便完全合并访问,但 GPU 的内存架构并不是真正为发出如此小的读取的单个线程设计的——也许每个线程实际上会读取(至少)32 位,然后将其中的一半扔掉。在任何情况下,结果都是内存总线没有得到充分利用。更有效的半精度降低将使每个线程通过单个事务读取多个打包值,例如通过读取 32 位 half2。这是我们在某个时候必须教编译器的一个技巧。这也不是一个新问题——如果你编写一个对 8 位或 16 位整数求和的程序,你也没有充分利用内存总线,即使在绝对运行时,它仍然比对相同数量的 32 求和更快-位整数。我们如何验证这个分析是正确的?我们可以编写一个程序,使用 map-reduce 组合将 u32 值数组解释为压缩的 f16s:这可以在仅仅 1.5ms 的时间内将 10 亿个 f16s(即 50 亿个 u32s)相加 - 一个 1.93x对 f32 求和的加速。
除非您知道自己在做什么,否则您可能不应该在代码中使用 f16。它们的数值属性绝对会让你大吃一惊。互操作性很尴尬,性能甚至还没有那么好。但是从语言的角度来看,它们现在得到了完全支持,希望在不久的将来我们能让它们运行得更快。但我怀疑我们会添加更多位,所以数字总是可疑的。