我想要一台优秀的并行计算机
我想要一台优秀的并行计算机
2025年3月21日
你电脑中的 GPU 比 CPU 强大约 10 到 100 倍,具体取决于工作负载。对于实时图形渲染和机器学习,你正在享受这种能力,而在 CPU 上执行这些工作负载是不可行的。为什么我们没有利用这种能力来处理其他工作负载呢?是什么阻止了 GPU 成为更通用的计算机?
我认为主要有两个因素阻碍了它。一是贫乏的执行模型,这使得某些任务难以甚至不可能高效地完成;GPU 擅长处理具有可预测形状的大块数据,例如密集矩阵乘法,但在工作负载动态变化时表现不佳。其次,我们的语言和工具不够完善。对并行计算机进行编程要困难得多。
现代 GPU 也极其复杂,并且正在迅速变得更加复杂。诸如 mesh shaders 和 work graphs 之类的新功能是进步两步,后退一步;对于每个新功能,都存在一个未完全支持的基本任务。
我相信一种更简单、更强大的并行计算机是可能的,并且在历史记录中存在迹象。在一个略有不同的世界中,我们现在将拥有这些计算机,并且正在为在各种各样的任务中良好运行而设计算法和编写程序。
去年四月,我在 UCSC CSE 项目中做了一个题为相同标题的 colloquium(视频)。这篇博客是该报告的补充。
复杂 GPU 程序的内存效率
多年来,我一直在开发 Vello,这是一款先进的 2D 矢量图形渲染器。CPU 以简化的二进制类 SVG 格式上传场景描述,然后计算着色器负责其余的工作,最终生成 2D 渲染图像。计算着色器解析树结构,为 stroke expansion 执行高级计算几何,以及用于分箱的类似排序的算法。在内部,它本质上是一个简单的编译器,为每个 16x16 像素图块生成一个单独的优化字节码程序,然后解释这些程序。它无法做到的事情,也是我越来越感到沮丧的问题,是以有限的内存运行。每个阶段都会产生中间数据结构,并且这些结构的数量和大小以不可预测的方式取决于输入。例如,更改编码场景中的单个变换可能会导致截然不同的渲染计划。
问题在于,中间结果的缓冲区需要在启动管道之前(在 CPU 控制下)分配。有很多不完善的潜在解决方案。我们可以在开始渲染之前在 CPU 上估计内存需求,但这既昂贵又不精确,从而导致失败或浪费。我们可以尝试渲染,检测失败,并在超过缓冲区时重试,但是从 GPU 读取回 CPU 是一个很大的性能问题,并且会对我们与之交互的其他引擎造成很大的架构负担。
特定问题的细节很有趣,但超出了本博客文章的范围。感兴趣的读者可以参考 Potato 设计文档,该文档探讨了在尊重有界 GPU 资源的同时,在 CPU 上进行调度的程度,同时使用 GPU 进行实际的像素处理。它还涉及标准 GPU 执行模型的一些最新扩展,所有这些扩展都复杂且不可移植,并且似乎都不能完全解决问题。
从根本上讲,不需要分配大缓冲区来存储中间结果。由于它们将被下游阶段使用,因此将它们放入队列中效率更高,队列的大小足以保持足够的正在进行的项目以利用可用的并行性。许多 GPU 操作在内部都作为队列工作(标准的顶点着色器/片元着色器/rasterop 管道是经典的例子),因此这是一个将底层功能暴露给应用程序的问题。GRAMPS 2009 年的论文提出了这个方向,CUDA 的前身 Brook 项目也是如此。
对于在有限内存中运行类 Vello 算法有很多潜在的解决方案;如今,大多数解决方案在硬件上都存在致命缺陷。推测一下可以解锁该功能的更改会很有趣。值得强调的是,我并没有受到可以利用的并行量所阻碍,因为我将问题分解为前缀和变体的方法很容易扩展到数十万个线程。相反,无法将整体组织成并行运行的阶段,这些阶段通过队列连接,队列经过调整以仅使用保持一切顺利所需的缓冲区内存量,而不是通过管道障碍分隔的大型调度的计算着色器执行模型。
过去的并行计算机
今天缺乏一台好的并行计算机尤其令人沮丧,因为过去有一些有希望的设计,由于各种复杂的原因未能流行起来,使我们拥有过于复杂和受限的 GPU,以及极其有限但高效的 AI 加速器。
Connection Machine
我列出这个不是因为这是一个特别有希望的设计,而是因为它以最清晰的方式表达了对一台好的并行计算机的梦想。第一台 Connection Machine 于 1985 年发布,包含多达 64k 个处理器,这些处理器以超立方体网络连接。即使以今天的标准来看,单个线程的数量也很大,尽管每个处理器都非常弱。
也许最重要的是,CM 激发了对并行算法的巨大研究。Blelloch 关于 prefix sum 的开创性工作主要是在 Connection Machine 上完成的,并且我发现早期关于 sorting on CM-2 的论文非常引人入胜。
Connection Machine 1 (1985) at KIT / Informatics / TECO • by KIT TECO • CC0
Cell
另一个重要的开创性并行计算机是 Cell,它于 2006 年作为 PlayStation 3 的一部分发布。该设备的出货量相当可观(约 8740 万台),并且具有令人着迷的应用,包括 high performance computing,但它是一条死胡同;Playstation 4 切换到相当普通的 Radeon GPU。
Cell 中可能最大的挑战之一是编程模型。在 PS3 上发布的版本中,有 8 个并行内核,每个内核具有 256kB 的静态 RAM,并且每个内核具有 128 位宽的向量 SIMD。程序员必须手动将数据复制到本地 SRAM 中,然后内核将在其中进行一些计算。几乎没有或根本没有对高级编程的支持;因此,想要以该平台为目标的人必须煞费苦心地构建和实现并行算法。
综上所述,Cell 基本上满足了我对“一台好的并行计算机”的要求。各个内核可以有效地运行任意程序,并且有一个全局作业队列。
Cell 的总吞吐量约为 200 GFLOPS,这在当时令人印象深刻,但与现代 GPU 甚至现代 CPU 相比都相形见绌(Intel i9-13900K 大约为 850 GFLOPS,中高端 Ryzen 7 为 3379 GFLOPS)。
Larrabee
也许 GPU 设计历史上最令人心酸的未实现的道路是 Larrabee。2008 SIGGRAPH paper 提出了一个令人信服的案例,但该项目最终失败了。很难确切地说出原因,但我认为这可能是英特尔执行不力造成的,并且如果坚持不懈并进行几次迭代以改进原始版本的缺点,它很可能会成功。Larrabee 的核心是标准的 x86 计算机,具有宽(512 位)SIMD 单元,并且只有少量特殊硬件来优化图形任务。大多数图形功能都是在软件中实现的。如果它成功了,它将很容易满足我的愿望;工作创建和排队是在软件中完成的,并且可以在精细的粒度级别上完全动态。
Larrabee 的片段仍然存在。即将推出的 AVX10 指令集是 Larrabee 的 AVX-512 的演变,并支持 32 条 f16 操作通道。实际上,其创建者之一 Tom Forsyth 认为 Larrabee 确实没有失败,但其遗产是成功的。遗产的另一个有价值的方面是 ISPC,Matt Pharr 的博客 The story of ispc 阐明了 Larrabee 项目。
Larrabee 的问题之一可能是功耗,这已成为并行计算机性能的限制因素之一。完全连贯(总存储顺序)的内存层次结构虽然使软件更容易,但也增加了系统的成本,并且从那时起,我们已经获得了大量关于如何在较弱的内存模型中编写高性能软件的知识。
另一个肯定阻碍 Larrabee 的方面是软件,这始终具有挑战性,尤其是在创新方向上。驱动程序没有公开高度可编程硬件的特殊功能,并且传统三角形 3D 图形场景的性能不尽如人意。即便如此,在由标准 OpenGL 接口驱动的涉及大量抗锯齿线的 CAD 工作负载方面,它做得很好。
不断变化的工作负载
即使在游戏中,计算也正在成为总工作负载中越来越大的比例(对于 AI,一切都是如此)。Chips and Cheese 对 Starfield 的分析表明,大约一半的时间用于计算。Nanite 渲染器甚至使用计算来光栅化小三角形,因为硬件仅对于大于一定尺寸的三角形才更有效。随着游戏进行更多的图像过滤、全局照明和高斯溅射等图元,这种趋势几乎肯定会持续下去。
2009 年,Tim Sweeney 发表了一个发人深省的演讲,题为 The end of the GPU roadmap,其中他提出 GPU 的概念将完全消失,取而代之的是高度并行的通用计算机。这种情况尚未发生,尽管朝着这个方向取得了一些进展:Larrabee 项目(如上所述),具有开创性的 cudaraster 2011 年的论文完全在计算中实现了传统的 3D 光栅化管道,并且发现(简化了很多)它比使用固定功能硬件慢大约 2 倍,以及最近基于 RISC-V 内核网格的学术 GPU 设计。值得注意的是,Tellusim 最近的 update 建议在现代硬件上,计算中的类 cudaraster 渲染可以接近对等。
Andrew Lauritzen 在 2017 年发表了一篇出色的演讲,Future Directions for Compute-for-Graphics,其中重点介绍了将高级计算技术融入图形管道的许多挑战。从那以后取得了一些进展,但这说明了我在本博客文章中提出的许多相同问题。另请参见 Josh Barczak 的评论,该评论还链接了 GRAMPS 的工作,并讨论了语言支持方面的问题。
前进的道路
我可以看到几种从当前状态到一台好的并行计算机的方法。每种方法基本上都选择了一个可能走上正轨但脱轨的起点。
大内核网格:Cell 重生
Cell 最初的承诺仍然具有一定的吸引力。现代高端 CPU 芯片拥有超过 1000 亿个晶体管,而性能相当不错的 RISC CPU 可以用更少的数量级制造。为什么不在芯片上放置数百甚至数千个内核?为了获得最大吞吐量,在每个内核上放置一个向量 (SIMD) 单元。实际上,至少有两个基于此思想的 AI 加速器芯片:Esperanto 和 Tenstorrent。我对后者特别感兴趣,因为它的 software stack 是开源的。
也就是说,肯定存在挑战。仅 CPU 是不够的,它还需要高带宽本地内存以及与其他内核的通信。Cell 如此难以编程的原因之一是本地内存很小并且需要显式管理 - 你的程序需要通过网络进行显式传输才能将数据移入和移出。CPU(和 GPU)设计的发展趋势是虚拟化一切,以便所有内核共享一个大内存池的抽象。你仍然需要使你的算法具有缓存感知能力以提高性能,但如果不是,程序仍然可以运行。足够聪明的编译器 可能 可以将问题的详细描述改编为实际硬件(这是 Tenstorrent 的 TT-Buda 堆栈所采用的方法,专门用于 AI 工作负载)。类似于通过 VLIW 利用指令级并行性,Itanium 充当了一个警示故事。
从我对 Tenstorrent 文档的解读来看,矩阵单元仅限于矩阵乘法和一些支持操作(例如转置),因此尚不清楚对于 2D 渲染中所需的复杂算法,它是否会显着加速。但我认为值得探索,看看它可以被推到多远,以及对矩阵单元进行实际扩展以支持置换等等是否可以解锁更多算法。
大多数“大内核网格”设计都针对 AI 加速,这有充分的理由:它对原始吞吐量有很高的需求,并且功耗成本低,因此传统 CPU 方法的替代方案很有吸引力。有关该领域的精彩概述,请参见 Ian Cutress 的 New Silicon for Supercomputers 演讲。
从 GPU 端运行 Vulkan 命令
对现有 GPU 的相对较小的改进是能够从安装在 GPU 上并与着色器共享地址空间的控制器分派工作。在其最通用的形式中,用户将能够在控制器上运行线程,该线程可以运行完整的图形 API(例如,Vulkan)。编程模型可能与现在类似,只是提交工作的线程靠近计算单元运行,因此延迟大大降低。
在最早的形式中,GPU 不是分布式系统,而是协处理器,与主机 CPU 的指令流紧密耦合。如今,工作通过异步远程过程调用的等效项发送到 GPU,端到端延迟通常高达 100µs。该提案本质上要求恢复到较少的分布式系统模型,在该模型中,可以以更精细的粒度和对数据更高的响应性来有效地发出工作。对于动态工作创建,延迟是最重要的阻碍。
请注意,GPU API 正在缓慢地发明一种更复杂、更有限的版本。虽然无法直接从着色器运行 Vulkan API,但使用最新的 Vulkan 扩展 (VK_EXT_device_generated_commands),可以从着色器将一些命令编码到命令缓冲区中。Metal 也具有此功能(有关可移植性的更多详细信息,请参见 gpuweb#431)。值得注意的是,运行间接命令以递归生成更多工作的能力是缺失的功能之一;设计师似乎没有铭记 Hofstadter。
考虑从着色器直接运行 Vulkan API 实际上很有趣。由于 Vulkan API 是用 C 表示的,因此要求之一是能够运行 C。这正在实验性地完成(参见 vcc 项目),但尚未实用。当然,CUDA 可以 运行 C。CUDA 12.4 还支持 conditional nodes,并且从 12.0 开始,它支持 device graph launch,这大大降低了延迟。
Work graphs
Work graphs 是 GPU 执行模型的最新扩展。简而言之,程序被构造为节点(内核程序)和边缘(队列)的图,所有节点和边缘都在并行运行。当节点生成输出时,填充其输出队列,GPU 会分派内核(以工作组粒度)以进一步处理这些输出。在很大程度上,这是对 GRAMPS 思想的现代重新发明。
虽然令人兴奋,并且很可能对越来越多的图形任务有用,但 work graphs 也存在严重的局限性;我研究了我是否可以使用它们来设计现有的 Vello,并发现了三个主要问题。首先,它们不容易表达连接,其中节点的进度取决于来自两个不同队列的同步输入。Vello 广泛使用连接,例如一个内核来计算绘制对象的边界框(聚合多个路径段),另一个内核来处理该边界框内的几何体。其次,不能保证推送到队列中的元素之间的顺序,并且 2D 图形最终确实需要排序(老虎的胡须必须绘制在老虎的脸上)。第三,work graphs 不支持可变大小的元素。
缺乏排序保证尤其令人沮丧,因为传统的 3D 管道 确实 保持排序,除其他原因外,是为了防止 Z 冲突伪像(有关 GPU 硬件如何保留混合顺序保证的有趣讨论,请参见 A trip through the Graphics Pipeline part 9)。使用新功能无法忠实地模拟传统的顶点/片元管道。显然,在并行系统中维护排序保证是昂贵的,但理想情况下,有一种方法可以在需要时选择加入,或者至少将 work graphs 与另一种机制(某种形式的排序,可以在 GPU 上有效地实现)结合起来,以根据需要重新建立排序。因此,我认为 work graphs 是进步两步,后退一步。
CPU 收敛进化
从理论上讲,当运行高度并行的工作负载时,传统的多核 CPU 设计正在做与 GPU 相同的事情,如果针对效率进行了全面优化,则应该具有竞争力。这可以说是 Larrabee 的设计摘要,也是最近学术工作(如 Vortex)的动机。可能最大的挑战是电源效率。作为一种普遍趋势,CPU 设计正在分为优化单核性能的设计(性能内核)和优化电源效率的设计(效率内核),这两种类型的内核通常都存在于同一芯片上。随着 E-cores 变得越来越普遍,旨在大规模利用并行性的算法可能会开始获胜,从而激励提供更大数量的效率越来越高的内核,即使对于单线程任务而言,这些内核的功率不足。
这种方法的优点在于它不会更改执行模型,因此仍然可以使用现有的语言和工具。不幸的是,大多数现有语言在 SIMD 和线程级别上都不能很好地表达和利用并行性 - 着色器具有更有限的执行模型,但至少很清楚如何有效地并行执行它们。对于线程级并行性,避免上下文切换造成的性能损失具有挑战性。希望像 Mojo 这样的较新语言会有所帮助,并且可能会适应类似 GPU 的执行模型。
我怀疑这种方法实际上会与 GPU 和 AI 加速器竞争,因为与 GPU 相比,每瓦特的吞吐量存在巨大的差距 - 大约一个数量级。此外,GPU 和 AI 加速器也不会停滞不前。
也许硬件已经存在?
可能当前正在运送的硬件符合我对一台好的并行计算机的标准,但其潜力受到软件的阻碍。GPU 通常板载一个“命令处理器”,该处理器与主机端驱动程序协同工作,将渲染和计算命令分解为由实际执行单元运行的块。不变的是,此命令处理器是隐藏的,并且无法运行用户代码。打开它可能会很有趣。Hans-Kristian Arntzen 在开源驱动程序中实现 work graphs 的注释中对这一点有所了解:Workgraphs in vkd3d-proton。
GPU 设计在硬件中内置了多少以及命令处理器完成了多少各不相同。可编程性是使事物更灵活的好方法。主要的限制因素是围绕此类设计的保密性。即使在使用开源驱动程序的 GPU 中,固件(在命令处理器上运行的固件)也受到严格限制。当然,一个相关的挑战是安全性;向用户代码开放命令处理器会大大增加漏洞利用面。但是从研究的角度来看,除了安全问题之外,探索什么是可能的应该很有趣。
另一个有趣的方向是“加速处理单元”的兴起,它将 GPU 和强大的 CPU 集成在相同的地址空间中。从概念上讲,这些类似于集成显卡芯片,但这些芯片很少具有足够的性能来引起人们的兴趣。从我所见的情况来看,由于上下文切换开销,在此类硬件上运行现有 API(用于计算着色器的 Vulkan 或 OpenCL 的现代变体之一)在将工作同步回 CPU 时不会有显着的延迟优势。优先级较高的线程或专用线程可能会快速处理 GPU 端任务放置在队列中的项目。关键思想是在完全吞吐量下运行的队列,而不是具有潜在巨大延迟的异步远程过程调用。
复杂性
退一步说,GPU 生态系统的主要特征之一是令人眼花缭乱的复杂程度。有核心并行计算机,然后有许多特殊功能硬件(并且范围正在增加,尤其是在射线追踪等新功能中),然后有笨拙的机制来安排和运行工作。这些机制从基本的计算着色器调度机制开始(x、y、z 维度的 3D 网格,每个维度 16 位),然后使用各种 indirect command encoding 扩展对其进行增强。
Work graphs 也属于使执行模型复杂化以解决原始 3D 网格的局限性的类别。最初我对它们的前景感到兴奋,但是当我仔细观察时,我发现它们不足以表达 Vello 中的任何生产者/消费者关系。
还有很多偶然的复杂性。有多个相互竞争的 API,每个 API 都有细微不同的语义,这使得编写一次代码并使其正常工作尤其困难。
CUDA 正在添加许多新功能,其中一些功能提高了自主性,正如我一直想要的那样,并且图形 API 倾向于采用 CUDA 中的功能。但是,这些生态系统之间也存在很多差异(work graphs 无法轻松地适应 CUDA,并且图形着色器不太可能很快获得独立的线程调度)。
GPU 生态系统的复杂性具有许多下游影响。驱动程序和着色器编译器存在错误并且 insecure,并且可能没有真正修复它的途径。核心 API 在功能和性能方面往往非常有限,因此需要在运行时检测到大量扩展,并选择最合适的排列。反过来,这使得更有可能遇到仅在特定功能组合或特定硬件上出现的错误。
所有这些与 CPU 世界形成了鲜明的对比。现代 CPU 也非常复杂,拥有数十亿个晶体管,但它植根于更简单的计算模型。从程序员的角度来看,为 250 亿个晶体管的 Apple M3 编写代码与为大约 48,000 个晶体管的 Cortex M0 编写代码并没有什么不同。类似地,低性能的 RISC-V 实现是一个合理的学生项目。显然,M3 通过分支预测、超标量发布、内存层次结构、op 融合和其他性能技巧做了更多的事情,但它可以识别出与更小更简单的芯片做了同样的事情。
过去,存在将专用电路替换为通用计算性能的经济压力,但这些激励措施正在发生变化。基本上,如果你要优化晶体管的数量,那么效率较低的通用计算几乎可以一直保持繁忙,而只有在工作负载中具有足够高的利用率时,才能证明专用硬件是合理的。但是,随着 Dennard 缩放的结束,并且我们更多地受到功耗而不是晶体管数量的限制,专用硬件开始赢得更多;如果工作负载未使用它,则可以简单地将其断电。纯粹的 RISC 计算模型的日子可能已经结束。我 想 看到的是用敏捷核心(可能是 RISC-V)来代替它,作为一堆专用加速器扩展的控制功能。这当然是 Vortex 项目的模型。
结论
在他的退休前不久的演讲中,Nvidia GPU 架构师 Erik Lindholm said(在工作创建和排队系统的上下文中)“我的职业生涯一直在使事情变得更加灵活、更可编程。它还没有完成。我感觉还需要做一步,我已经在 Nvidia Research 中追求了很多年。”我同意,我自己的工作将从中受益匪浅。现在他已经退休,尚不清楚谁将接过衣钵。可能是 Nvidia 推出了一种新方法来颠覆其以前的产品线,就像他们过去所做的那样。可能是新兴的 AI 加速器制造了一个巨大的低功耗处理器网格,该处理器带有矢量单元,而该矢量单元恰好是可编程的。可能是 CPU 效率内核不断发展,以变得如此高效以至于与 GPU 竞争。
或者可能根本不会发生。按照目前的轨迹,GPU 将以增加复杂性为代价,在现有图形工作负载上挤出增量改进,而 AI 加速器将专注于提高 slop 生成的吞吐量,而忽略其他一切。
无论如何,对于求知欲强的人来说,有机会探索存在优秀的并行计算机的平行宇宙;可以在 FPGA 上模拟架构,例如 Vortex,并且可以在多核宽 SIMD CPU 上对算法进行原型设计。我们还可以开始考虑这种机器的适当编程语言是什么样的,尽管没有真正的硬件可以运行它令人沮丧。
在优秀的并行计算机上取得的进展将有助于我自己的小小的工作,即尝试制作一个资源需求适中的完全并行 2D 渲染器。我不得不想象它也将有助于 AI 工作,可能会解锁无法在现有硬件上运行的稀疏技术。我还认为,存在一个算法的黄金时代,这些算法 可以 并行化,但在当前 GPU 上并不是胜利,等待着被开发。