Transcription
大家好,感谢各位的到来。今天我们非常荣幸地邀请到了罗汉。他目前是斯坦福大学计算机科学专业的四年级博士生,师从 Alex Akin 和 Fred Kyst。他同时也是 NVIDIA 的兼职研究员。他的研究兴趣主要集中在编程语言和计算机系统领域,尤其关注并行和加速计算系统。他的工作已被诸如 Legate(一个分布式科学计算运行时系统)等项目采用,并获得了包括 NSF 研究生奖学金和 NVIDIA 研究生奖学金在内的多项奖项,以及在 PLDI 和 SC 等顶级会议上发表的多篇论文。
那么,请大家和我一起热烈欢迎他,今天他将与我们分享关于“高效编程加速计算系统”的见解。
[掌声]
感谢你为我做的介绍。这通常比我自己会说的个人简介要详细得多。我是斯坦福大学的一名博士生,与 Alex Aken 和 Fred Kilston 一起工作。非常感谢你们邀请我来这里演讲。我将要谈论我的工作,其重点是让高性能加速机器的编程变得更加容易。
目前,各种加速计算系统正在被构建,包括像现代 GPU 这样的独立加速器,包含多个加速器的单节点,以及包含数十万个互连加速器的大规模现代超级计算机,所有这些都需要被编程为一个单一的系统。我们非常关心这些机器的编程,因为我认为它们是我们人类用来应对关键挑战和理解我们周围世界的工具的最前沿。这意味着运行重要的计算工作负载,例如计算科学中的物理学、生物学、化学、气候科学,以及日益重要的机器学习和人工智能工作负载,以及大规模数据分析,以理解我们社会产生的数据量。
随着时间的推移,这些机器变得越来越强大,这意味着 [清嗓子] 它们提供了越来越多的浮点运算能力、更多的内存容量和带宽,这意味着我们可以运行我们关心的应用程序的更大版本。我们可以运行更高分辨率的模拟,并且可以以前所未有的速度运行它们。
然而,我认为在过去的十年里,这些改进已经不再是免费的了。虽然这些机器提供了更高的性能,但它们的编程也变得越来越复杂。这意味着我们需要编写新的代码,开发新的优化来真正释放这些机器提供的性能。我们不能只是编写相同的代码,然后期望它在下一代架构上运行得快一倍。这种复杂性出现在堆栈的各个层面,从单个加速器到它们作为分布式机器的聚合。
当我们观察单个加速器时,它们变得越来越异构和专业化。像 Volta GPU 这样的老一代加速器看起来像是一组相对同质的功能单元,用于执行应用程序中的不同计算。而像 Blackhole GPU 这样的现代加速器包含占据芯片大部分面积的大型固定功能单元,例如张量核心或张量内存加速器,用于执行特定计算,如批量矩阵乘法或块数据移动。
这种日益增长的专业化和对性能的追求,导致了我们多年来依赖的软件和硬件之间相对稳定的界限开始打破,并消除了其中一些通常会带来高昂的能源和面积成本的通用性。这种界限模糊的结果是,许多硬件曾经免费为我们软件做的事情,例如拥有稳定的 ISA 或在硬件中进行一些指令同步,现在被迫由软件来完成,这使得实现高性能变得更加困难,也使得未来移植这些机器变得更加困难。
然后我们来看分布式机器。我们将这些单独复杂的处理器聚合到大型复杂的层次化机器中,其中现代超级计算机包含多个节点。每个节点本身就是一个层次化机器,可能包含一个多核 CPU,然后连接着多个加速器,例如多个 GPU。这些加速器通过层次化网络连接,您拥有快速的内部通信,以及跨机架的稍慢的通信。在编程这些机器时,您会遇到许多相似和不同的挑战,例如:程序员如何实际讨论将数据和计算分布到这台机器上?程序员如何使用不同数据块的正确通信链路来协调通过这个层次化网络的数据移动,并在可能的情况下重叠通信和计算?最后,他们需要实际同步这台机器的不同子集,以实际维持应用程序的正确性。
因此,当我们审视这些机器的编程时,它确实是一个全栈问题,您需要拥有从运行在处理器上的单个内核到构建在其上方的层,以高效地协调分布式机器上的计算。当您结合这些挑战时,您最终会得到这样的系统,编程这些系统确实处于我们认知能力的边缘,并且在这里实现高性能非常困难。
因此,我一直在研究堆栈各层的问题,以实现高效加速编程的愿景,并开发了技术,使我们能够编写能够针对这些复杂机器的高级可组合库,然后以最小的努力将这些应用程序移植到未来。具体来说,在我的博士研究中,我一直在处理这里的各个层面:我构建了高级可组合的分布式库,为程序员提供像标准 NumPy 和 SciPy 这样的简单接口,但它们可以自动扩展到分布式机器,并且可以像在顺序机器上一样相互组合。然后,在此之下,我研究了各种分布式运行时系统技术,以实际支持这些高级库,并包含一些关键的编程语言思想,并使这些库能够高效且正确地组合。然后,在堆栈的底部,我研究了系统,以便性能工程师能够构建在这些机器上运行的高性能内核,包括允许我们在整个集群上编写内核的系统,以及针对单个加速器上工作的挑战。
因此,在这次讲座中,我将只讨论这些工作中的一部分,特别是,我将讲述一个关于我如何努力解决高效编程这些机器的问题的故事,并且我将从高层次上谈论两个项目。首先是 Distill,为我将在讲座后面深入探讨的两个独立问题奠定基础,这些问题将与我之前强调的主题相同,即专注于高效地协调我们的分布式计算,然后构建运行在这些机器上的高性能内核。
好的。我在博士期间做的第一项工作是解决编写密集和稀疏张量代数内核并在整个集群上运行这些内核的问题,这在机器学习和科学工作负载中都有应用。我在博士期间做的第一个项目是构建 Distill 编译器,它提供了一个高级编程语言和相关的编译基础设施来开发这类分布式张量代数计算。Distill 的关键思想是将我们如何分解计算和分解分布式机器上的数据的方法分开,并将它们分解成两种独立的高级语言,然后编译器将把它们混合在一起,并输出在这台机器上运行良好的代码。
这最终被证明是一个相当不错的想法,数据和计算分布的分离足够具有表现力,使我们能够用一段非常紧凑的代码写下分布式矩阵乘法算法的悠久历史,从 1969 年的 Cannon 算法到现代的通信避免算法,与您通常看到的描述这些算法的低级、充满通信的代码相比。然后,我们证明了我们可以从这些规范生成经过高度调优的高性能代码,成为同类系统中的第一个能够实现与手工调优代码相媲美的性能。这张图显示了上一张幻灯片中生成的每个实现,它们在多 GPU 系统上生成并运行,我们可以在 1024 个 GPU 上实现与专家调优基线相媲美的性能。然后,由于我们有了编译器,这种灵活性使我们能够为尚未经过人工调优的内核实现高性能,例如这个更大的张量收缩。
我在 DSL 方面的工作使我与 NVIDIA 的一些研究人员建立了联系,他们已经为密集数组编程构建了一个分布式 NumPy 库。因此,他们可以将 NumPy 程序在分布式机器上运行,并且他们想在此基础上构建一个稀疏库。这样您就可以在同一个系统中进行分布式密集和稀疏数组编程。我们去做的关键想法是使用 Distill 来生成这样一个库所需的所有分布式稀疏矩阵内核,因为有太多不同的稀疏矩阵格式、内核以及您想要在上面运行的后端。编译器极大地加速了构建过程。
一旦我生成了这些实现,我们就意识到实际上存在一些更深层次的问题,关于我们如何以一种明智的方式将这些库组合在一起,以及如何构建能够实现高并发的独立分布式库。这是一个与编写单个高性能内核非常不同的问题。因此,我们在这一方面取得了重大进展,我们构建了一个名为 Legate 生态系统的分布式库生态系统,它看起来就像标准的基于集合的 Python 库,但可以自动扩展到分布式和加速机器。我在这里展示了一些 Legate 的代码,其中您导入的是我们的库 CuNumeric 和 Legate Sparse,而不是导入 NumPy 和 SciPy,然后以正常方式编写其余代码,一个标准的共轭梯度求解,这段代码将从单个 GPU 扩展到 192 个 GPU,并实现与低级手工调优系统(如用于分布式稀疏线性代数的 PETSc 库)相媲美的性能。
一个非常重要的组成部分是可组合性,我可以将一个库生成的分布式数据“扔”到另一个分布式库中,生成更多数据,然后将这些数据传回 CuNumeric,所有这些都能正常工作,而无需程序员做任何事情,并且仍然能与这些系统相媲美。此后,NVIDIA 的工程师们已经以类似的方式构建了大量的库,并开始构建这个库的生态系统,程序员可以真正地将它们组合起来,编写普通的 Python 代码,并随着时间的推移进行扩展。
为了在不产生标准组合成本的情况下实现这种组合,我和其他人开发了各种分析工具,可以动态分析这些独立库发起的计算,并利用即时特化策略将它们编织在一起,以匹配同一程序的专家手工实现。这些分析包括跨库的分布式数据共分区、融合依赖计算和生成特化内核,以及发现跨库边界所需的数据依赖和数据移动。一旦您就特定软件应如何组合做出正确的决定,获得良好的性能就需要高效地进行这种协调和分析,这样我们就不会从分析中损失比从性能优势中获得的更多。然后,我们实际上需要这些内核本身非常高效,能够在我们的库中运行,并在各个处理器上运行。
因此,这两点是我今天将深入探讨的主题。特别是,在本次讲座的第一个深入探讨部分,我想专注于高效协调这些动态分析,以实现这类可组合的高级库。我已经开发了几种这样的技术,我将只介绍其中一种,它侧重于分布式软件堆栈最低层编程系统的效率,该层实际上协调了机器上的计算和数据移动的物理执行。
为了在我们的高性能机器上协调这类并行计算,存在两种主要的编程模型来实现这一点,它们构成了这个最低层的编程基础。第一个,我已经暗示过,是基于任务的编程模型。基于任务的编程模型暴露了一个非常声明式的接口,它们被组织成大量短暂的、原子用户定义的函数,称为任务,它们由对应于通信和同步的依赖边连接。因此,通常基于任务的编程系统有一种声明式的方法来描述这些依赖关系是什么,然后系统负责执行满足这些依赖关系的程序。存在着悠久的编程模型历史,它们实现了这种基于任务的系统,以及像我之前展示的那些更高级的系统,它们对应于编写程序并生成这种风格的程序。
这里的第二个编程模型家族是基于 Actor 的编程模型,它们具有更强的操作结构。基于 Actor 的编程模型被组织成一组长生命周期的 Actor 对象,它们通过消息传递进行通信。因此,Actor 可以相互发送消息,程序员控制事件发生的主要方式是实现一个消息处理器,该处理器说明当 Actor 收到特定消息时应该做什么。这些 Actor 在收到消息时可以发送更多消息,可以运行一些应用程序计算,或者可以更新任何任意的内部状态。同样,存在着悠久的编程模型历史,它们实现了基于 Actor 的系统,我认为如果您想实现类似裸机高性能实现(如 MPI 或 Charm++)的模型,您可能会选择其中许多模型。
因此,为了比较和对比这些模型并获得我想要谈论的内容,我想用这两种模型实现同一个并行程序。这个并行程序将由这里简单的计算有向无环图(DAG)F1 到 F4 来描述,其中一些计算将被指定在 CPU 0 上运行,一些在 CPU 1 上运行。这是一个相当简单的程序,但我们已经可以看到,我想要谈论的许多挑战将在这个小程序中得到体现。
因此,在基于任务的编程模型中,这些图通常由目标应用程序或高级系统动态构建。因此,当应用程序运行时,我们将构建这个图。但在基于 Actor 的程序中,它们通常围绕静态定义的图进行组织。它通常来自我们脑海中的模拟结构或神经网络架构,我们知道哪些计算将相互依赖。我们将看到这两者如何在这些实现中出现。
因此,在基于任务的模型中,鉴于这些系统的声明性,这是相当直接的。一个简单的教学模型将暴露将计算启动到目标处理器上的能力。并且这个启动操作将返回一个事件,表示这个特定的启动任务何时完成。我可以以声明式的方式将它们链接起来,通过启动另一个依赖于 E1 的任务,这会限制 F2 的执行直到 E1 完成。我可以将它们全部链接起来以实现我的大型程序。我以一种非常声明式的方式完成了这一点。并且,是的,这个 DAG 是在我们运行程序时构建的,并且每次我们启动任务时,编程系统都会看到一个新的依赖关系并限制其执行。
因此,基于任务的编程模型具有一些非常好的特性。其中一个与可组合性有关,我很容易看到如何将这个程序嵌入到一个更大的程序中,只需连接内部和外部的依赖边,运行时系统和实现它的系统将负责在它们准备好时执行这些事情。它还具有我可以进行非常局部更改的特性。因此,我很容易看到如何,你知道,也许将一个任务移动到另一个处理器上,或者在这里添加另一个依赖边,只需修改这个程序的一小部分。
基于任务的编程系统的主要成本是它们的开销。由于这些图是动态指定的,这些编程模型通常是用运行时系统实现的,这些运行时系统一次一个任务地解释这些任务图,并且必须在它们出现时动态地调度和实现这些依赖关系。因此,这意味着我们将时间花在运行时系统协调我们的任务上,而不是做我们真正想做的应用程序工作 F1 到 F4,我们稍后会详细讨论这一点。
因此,如果我们现在在基于 Actor 的编程模型中这样做,我的主要原语是定义新 Actor 并让它们相互发送消息的能力。因此,在高性能的静态指定的程序构建中,我们将为我们系统中的每个处理器创建一个 Actor,并将 DAG 的部分分组,每个 Actor 处理器负责运行。自然地,这些边界之间存在依赖关系。因此,我们的 Actor 将需要相互发送消息。我们将通过实现这个消息处理器来控制这一点,说明当这些 Actor 收到消息时应该做什么。如果我实现这一点,我将得到以下代码,它会根据特定消息进行分支,然后执行一些工作。我们可以看看它实际上做了什么。因此,我将通过发送消息给 CPU 零来启动计算,以开始运行 F1。一旦 F1 完成,我们将向另一个 Actor 和我们自己发送消息以开始后续计算。然后这些后续计算开始。我们将完成,发送另一条消息。我们将运行 F3,发送另一条消息,然后最终我们执行了图中的所有计算。
因此,高效的 Actor 程序通常被构建为状态机,它们在状态空间中导航,接收消息,更改它们正在等待的消息,然后可能运行应用程序计算。这些状态机可以在其中有分支,对应于这些并行计算可能完成的不同顺序。因此,这可能会改变消息实际到达的顺序。因此,Actor 模型具有非常以处理器为中心的方法,允许发送最少的消息,并且简单的同步结构为我们提供了非常高的性能。然而,这种性能优化的负担通常落在用户身上,他们必须构建这些状态机并编写这种复杂的代码。很难想象如何将这个程序嵌入到一个更大的程序中,或者如果我更改了其中的一部分并想移动一个进程,将一个任务移到别处,它会蔓延并影响许多其他状态情况,并导致程序的一些非局部更改。
因此,我认为 Actor 和 Task 编程模型都处于这个前沿,其中基于任务的编程模型为您提供了一些非常好的可编程性属性,但代价是开销,这会影响它们的绝对性能和扩展行为。但是,基于 Actor 的编程模型为您提供了非常高的性能,但代价是存在一些可编程性挑战。
因此,在我们的工作中,我们实际上表明,你知道,不一定非得如此,我们可以定义一个新的前沿。我们可以获得基于任务的编程模型的许多好处,同时保留这些基于 Actor 的系统的性能。为了做到这一点,我认为我们表明,这些看似不同的 Actor 和基于任务的模型之间实际上存在着深刻的联系,特别是我们表明它们是彼此的对偶,这意味着我可以翻译这两种模型,所以我可以采用一个基于 Actor 的程序并将其转换为一个基于任务的程序,反之亦然,但更有趣的是,这种翻译实际上可以是高效的,我不是在做糟糕的翻译,我实际上会采用我的基于任务的程序并得到一个基于 Actor 的程序,它的运行效果就像专家从头开始实现该程序一样。这项工作受到了操作系统社区关于并发线程如何相互通信的思想的启发。
我将只谈论从基于任务的程序到基于 Actor 的程序的翻译。我想首先说明,今天它是如何完成的,基于任务的编程系统实际上已经用基于 Actor 的程序实现了,但它们被实现为一种运行时系统。因此,当我们的基于任务的程序在这里运行时,它将发出任务,这些任务将进入一个调度器 Actor,该调度器 Actor 将维护关于哪些任务已运行的元数据,然后当它们准备好时,它将向实际运行任务的工作线程发送消息,它们会将消息发送回调度器,表明任务已完成。与我们高效的 Actor 代码相比,为了运行任何特定的计算,可以清楚地看到需要更多的消息和协调。
因此,我们这项工作翻译的关键思想是针对特定程序对这个运行时系统本身进行特化。您将获得一个运行时系统,它基本上是为运行特定目标程序而构建的。如果您做得对,您将获得一个程序,它实际上导航一个类似的状态机,就像高效的 Actor 程序一样。并且,如果您尝试直接执行我刚才所说的,您会发现它并不完全可扩展。这是因为您想要导航的状态空间的大小,以高效地实现您的实际程序,其大小与特定并行计算图中存在的拓扑排序的数量成正比。拓扑排序的数量当然是指数级的。因此,对于这个简单的程序,只有两种不同的拓扑排序。因此,状态机相当小。但是,如果我为这个图增加一点复杂性,你知道,状态空间的大小会急剧增加。我甚至无法在一张幻灯片上显示所有内容。
因此,而不是直接表示这些状态空间,我们所做的是将用户提供的任务图编译成一组每个处理器上的 Actor,这些 Actor 被组织成解释器,并且这些解释器配置了不同的动态状态,其中状态是任何特定 Actor 需要负责的边,以便知道它可以开始并行工作。这个想法是,这些解释器是一种定制的运行时系统,专门用于我们想要运行的特定基于任务的程序。它们拥有的不同动态状态配置将隐式地表示我们可能显式运行的更大的状态机。因此,当我们相互发送消息以表明某些边已完成时,我们将隐式地导航我们高效 Actor 程序所做的相同状态空间。这种相对简单的编译策略实际上将产生相当高的性能,我将在下面展示。
因此,为了做到这一点,我将谈论我们如何实际衡量特定并行编程系统对其用户施加的开销。因此,我们将绘制一个系统的效率图。所以,这个百分比意味着 60% 的时间意味着我们花费 60% 的时间运行应用程序代码,而剩余的 40% 的时间实际上运行非应用程序代码。所以我们是在运行时系统中。因此,我们将跟踪在任何特定任务大小下,我们实际能有多高效。我们将跟踪这些系统何时跨越这个 50% 的线,这是一个任意的选择,但它是系统开始花费更少的时间进行应用程序工作而不是运行的截止点。它将花费更多的时间进行运行时系统工作而不是应用程序工作。
我们将通过采用一个固定的计算图来做到这一点,它看起来像这些类似模板的结构。在最左边,我们将拥有非常大的计算,持续时间可能在几毫秒之间。然后,当我沿着 x 轴移动时,我将保持相同的图,但我将使这些任务越来越小。因此,在最后,它们只运行几微秒。然后我们将测量我们的系统在这些不同大小上的实际效率。
因此,当我在这里绘制一个特定系统时,Realm,我们组开发的一个基于任务的编程系统,在一个节点的 HGPUs 上,我们得到这条曲线,你知道,在大型任务中,你可以获得相当高的效率,但随着任务越来越小,我们在运行时系统中花费的总时间比例越来越大,而不是实际的我们的应用程序。因此,如果我加入所有我们实际实验的系统,你会看到几个趋势。首先,基于 Actor 的系统表现最好,在尽可能长的时间内实现最低的开销,它们可以在这个特定的计算配置中高效地支持大约 10 到 20 微秒的计算。然后是基于任务的系统,它们明显较差,它们的性能特征不同,它们只能支持大约 100 微秒的计算,然后开销开始主导执行时间。您有 Ray,这是一个混合的 Actor-Task 系统,Ray 的表现不如,但它牺牲了一些性能特征来获得弹性等功能,这些功能在云计算环境中比这些其他 HPC 系统更有价值,这些 HPC 系统不具备执行此功能的能力。因此,Ray 的许多流行用例实际上具有非常长的运行任务,这些开销对性能并不重要。
因此,在我们的工作中,您看到的是,我们的编译策略实际上可以采用我们位于基于任务的性能特征内的系统 Legion 和 Realm,并将它们移到基于 Actor 的性能特征中,将它们施加的开销减少约 4 到 5 倍。这对实际的真实应用程序意味着,我们可以采用端到端的程序并极大地提高它们的强扩展性。因此,我有两个不同的流体模拟,用 Legion 编写。我可以采用,所以强扩展性是指我采用相同的问题大小,并将其分解到越来越多的处理器上,以尝试通过使用更多处理器来获得加速。因此,当我采用相同的问题并将其分解到越来越多的处理器上时,每个处理器的工作量就越来越小。因此,我能够走多远而不被开销主导,将极大地提高我在这些更大的问题规模上能获得的速度。
好的。因此,在讲座的这一部分,我告诉您了关于构建高性能并行应用程序的这两种编程模型之间的权衡,并且我向您展示了我们不必在这两种模型的易编程性和可编程性与性能之间做出选择,并且通过使用正确的编译策略,我们可以获得两全其美。
>> 是的。
>> 那么,总的来说,您似乎采用了基于任务的描述,然后根据 Actor 系统的运行时对其进行部分求值。
>> 嗯,是的,我们,是的,我们基本上特化了我们整个基于任务的运行时系统,它可以被看作是一组 Actor,我们得到了一个特化的运行时系统,它只能运行我们关心的特定程序,这削减了该运行时系统的许多通用部分,并为您提供了性能更高的东西。
>> 在它将要执行的分布式平台上,有什么重要的事情在特化或部分求值过程中被考虑进去吗?
>> 是的。所以,我没有特别谈论的是我们如何处理加速器。对于加速器,因为加速器有硬件支持同步,所以你可以做得更多。基本上,你想真正利用它来获得更好的性能。因此,通过进一步的特化,你可以深入了解这些加速器如何进行同步,并在可能的情况下使用这些硬件支持的同步原语,而如果你一次只看一个任务,你就无法做到这一点。你不知道谁将依赖于什么,以及这个程序将如何配置。
>> 那么,也许再深入一层,你可以告诉我留到最后。我不想打断太多,但是,您一开始谈到了在所有这些不同级别的系统组合之间的这种交互。似乎这里的选择可能会影响像本地内核级别的优化权衡。所以,这是否被考虑进去了?你研究过吗?
>> 是的。所以,我会说,在这个软件堆栈的这个点上,我们基本上将给定的基于任务的程序视为“真相之源”,其中任务和依赖关系是“不可改变的”,但更高级的系统,像 Legate 级别的编程模型负责生成这些任务和依赖关系,并且在该级别,我们可以进行融合等操作,并“制造”更好的内核,获取更多信息,但最终我们必须说,“这是我想在这台机器上运行的计算”,并且“这些是正确性所需的依赖关系”。
>> 没错。
>> 您在这里关注的是一系列任务大小,我可以看到为什么这很重要。但似乎这里还有另一个根本性的悖论,那就是,我们通常被教导通过表达更少的依赖关系来启用更多的并行性,但更少的依赖关系意味着需要跟踪您所处的可能状态的消息更多。这从来不是问题吗?您仍然想要您能获得的所有并行性,然后让运行时系统来处理吗?
>> 是的。所以,我将说,在这个特定的模型中,我谈论的是一种显式并行编程模型。因此,这里的依赖关系是我们假设的那些对正确性至关重要的依赖关系。因此,它们是精确的。这里没有额外的依赖关系。因此,任何其他满足这些特定依赖关系的排序都可以,你知道,可以被利用来获得性能机会。因此,你知道,像 Legion 这样的更高级系统在它们指定的依赖关系方面是精确的,不会添加任何不必要的。因此,这是 Legion 在编译到这个特定系统时表现良好的一个属性。
>> 是的。
>> 所以,您提到的开销看起来主要来自于为基于任务的系统使用集中式调度器。您能否比较一下您的方法与例如您为 [清嗓子] Reality 系统构建的去中心化方法?
>> 是的。所以,嗯,这是一个 HPU 的单节点。所以,所以技术上来说,我猜你可以有一个集中的,但我会说 Legion 运行时系统本身和 Realm 没有集中的调度器,特别是 Legion 将其依赖分析分布到所有节点的所有处理器上,独立地进行,所以每个节点只负责调度本地节点上的计算。所以,事实上,我没有这些数字,但你知道,以前的工作,不是我做的,而是来自这个团队,评估了一些具有集中式调度器的系统,并且基本上当你扩展到多个节点时,它们真的很难,因为你知道,你去了四个节点,那就是四倍的任务,调度器无法跟上,除非调度器本身也分片到系统中的节点上。
>> 嗯,是的,只是为了澄清,如果没有集中式调度器,那么,协调的开销来自哪里?
>> 开销来自所有层面。就像,你真的要通过不同的软件层调用更多的函数。你在队列里等待。你正在使用原子操作进行同步。你正在获取锁。就像当你深入到以微秒为单位运行计算时,你知道 CUDA 开销,CUDA 驱动程序在这里是一个限制。所以,当你查看减少这些系统的开销时,一切都像是“千刀万剐”。是的。好的,谢谢你的问题。
>> [哼哧声]
>> 嗯,好的,所以,我刚才讨论了这部分工作的一个组成部分,即高效地协调我们跨机器的并行计算。但是,为了在整个系统上获得最佳性能,对吧,我们实际协调的内核需要高性能,并从各个加速器中获得最佳性能。所以,这就是我接下来要谈论的,我将谈论让这些高性能内核的开发变得更具生产力,特别是谈论在现代张量核心 GPU 上实现高性能的一些近期挑战。这项工作是与我的同事 Ruponus Soy 合作完成的。
>> 所以,正如我在讲座开头提到的,我们现代 GPU 或加速器的每个子处理器现在都包含大型固定功能单元,它们占据了芯片的大部分面积,例如张量核心和图中的张量内存加速器,用于执行特定计算,如矩阵乘法或块数据移动。这些固定功能单元配有通用单元,用于执行更广泛的计算,如超越函数或普通算术。因此,在这些机器上,程序员的主要目标基本上是让这个张量在每个周期都保持运行,因为它包含了设备上 99.9% 的浮点运算能力。因此,它没有做任何事情的每个周期都是你浪费的金钱。
>> 因此,为了建立一些术语和背景,我想讨论一下这些设备的编程模型。所以,每个,所以,这是这里的一个子处理器,现代 GPU 包含大约一百个这样的子处理器。因此,我将只关注在一个子处理器上高效编程的挑战。因此,每个子处理器将托管一定数量的线程,这些线程实际上运行用户程序。最重要的事情是,硬件会将这些线程分组到称为 Warp 的组中。这有两个主要含义。因此,首先,这些 Warp 将独立于其他 Warp 向功能单元发出指令。因此,一个 Warp 将发出一个指令批次。假设其中一些使用张量核心,一些使用 TMA,然后硬件将协商发出的部分。然后,这些独立的 Warp 在寄存器文件中拥有固定数量的资源,它们可以访问。因此,如果我有更多的 Warp,我可能会获得更多的寄存器文件块来使用。
>> 好的,现在我向您展示了硬件,让我们进入一些编程挑战。因此,随着 GPU 上的这些固定功能单元变得越来越强大,在编程它们时,有两个优化变得越来越重要。第一个是软件流水线,这是一种调度优化,它重新排序指令的执行以增加我们循环的并行性和吞吐量。因此,为了谈论软件流水线,我这里有一个小程序,它是现代 Transformer 工作负载中使用的 Flash Attention 算法的核心。因此,这个特定的内核有三个操作。它有一个 GEM,它在张量核心上运行,一个 X,它必须在通用单元上运行,然后是另一个 GEM,它在张量核心上运行。因此,如果您天真地执行这个程序,您将得到“气泡”,因为数据依赖关系,我无法做任何其他事情,我只能等到这个 X 完成才能开始下一个 GEM。因此,我浪费了很多钱来运行这个程序。因此,软件流水线将重新排序此循环中的指令,特别是在迭代边界之间,在特定迭代中,我将执行迭代 i 的 GEM,同时执行迭代 i-1 的 X 和第二个 GEM,从而提供更多的重叠并增加此特定循环的吞吐量。
>> 第二个优化是 Warp 特化,这是一种空间并行化策略,它将在不同的 Warp(线程组)上执行 GPU 计算的不同组件。因此,Warp 特化有两个部分。首先是实际采用特定程序(如这个流水线)并说哪个 Warp 应该执行每个指令的策略,然后它有一个该策略的实际实现,通常看起来像这个空间流水线,其中负责某个计算的 Warp 将唤醒,执行其计算,将其写入某个全局内存,然后其他 Warp 将唤醒,然后它们的下游操作将运行,依此类推。
>> 因此,这些 GPU 的一个关键问题是,软件流水线和 Warp 特化的最佳策略在不同架构之间是变化的。因此,当 Hopper GPU 发布时,社区开发了各种各样的策略。基本上两年后,当 Blackwell GPU 发布时,所有这些策略都变得无用了。因此,我们不得不开发一些全新的策略来高效地运行 Blackwell GPU 上的代码。
>> 为了给您一个这些事情如何变化的例子。因此,我采用了那个对张量和 X 成本的特定比例效果很好的软件流水线。如果我只是让张量核心的速度提高一倍,这每年都在发生,而其余的代码,其余的单元速度不变,这基本上也在发生。这个流水线不再高效。我们现在实际上有气泡了。我们不再,你知道,我们不再获得良好的性能。您不必看这段代码,但如果您想再次获得更好的性能,您必须重新流水线这个循环。这里的许多结构都不同,有更多的阶段,循环内的依赖关系也发生了变化。
>> 然后对于 Warp 特化,架构配置会影响我们如何进行 Warp 特化。因此,在 Hopper GPU 上,像分离数据移动和计算这样的简单策略就足够了。但在 Blackwell GPU 上,需要非常复杂的策略,将不同的计算分解到不同的 Warp 中,并以奇怪的方式进行通信,才能获得良好的性能。
>> 因此,这些变化策略的影响可以在我们观察到的例如 Flash Attention 算法的演变中看到,我们在 Ampere 上从 Flash Engine 2 开始,在 Hopper GPU 发布一年后才发布了 Flash Engine 3,它基本上是针对 Hopper 调优的软件流水线和 Warp 特化策略,然后我们又等了一年才发布了 Flash Engine 4 算法,它提出了一个针对 Blackwell 调优的完全不同的软件流水线和 Warp 特化策略。因此,有一群专家非常擅长做这件事,但整个计算界不能等待,也不能浪费数万亿个周期来训练模型,比如等待有人告诉他们如何高效地使用这些机器。这是一个更大的问题,更大的迹象表明我们的软件基础设施正在落后于我们硬件的进化速度。
>> 因此,我认为这个问题的原因是软件流水线和 Warp 特化的组合理解得不够充分。软件流水线本身其实理解得很好,因为自 90 年代以来一直存在自动化和最优算法,但当您加入 Warp 特化时,这是一个只有启发式方法才为人所知的领域,专家会想出策略,编译器会尝试将这些策略应用于不同的代码片段,当您将它们放在一起时,您会失去很多这种组合性,一旦您添加了一个糟糕的 Warp 特化策略,您甚至无法获得这些算法提供的最优保证。因此,今天只有专家才能开发和手工编码这些策略,而我们的编译器根本无法与之竞争。
>> 因此,在我们的工作中,我们纠正了这种状况,我们表明我们可以推导出可证明最优的软件流水线和 Warp 特化策略,并解释为什么这些策略实际上是好的。我们意识到这是可能的,通过检查各种专家设计策略,并看到每个优化所做的决定在独立看待它们时并没有真正的意义。软件流水线的选择会影响 Warp 的特化方式,反之亦然。因此,我们以联合的方式对这些优化进行建模,在两种优化之间做出协同决策。
>> 这个优化问题的关键洞察是将其建模为一个调度问题,我们试图找到执行循环中指令的正确顺序。但同时,我们将约束有效调度的集合以满足资源分配问题,我们将不同的指令分配给不同的 Warp。因此,找到一个有效的调度将需要我们导航如何将不同的指令放在这些不同的 Warp 上,并利用它提供的约束。例如,如果我们把太多的指令放在一个 Warp 上,我们可能会超出它的寄存器预算,不得不把它们移到另一个 Warp 上。
>> 因此,我们将这个想法实现了一个名为 Twill 的系统,它联合发现了这些软件流水线 Warp 特化策略。Twill 接受一个机器模型,该模型描述了硬件的一些特性,例如运行张量核心所需的线程数,或者不同指令需要多少个周期。然后,它接收您需要优化的输入程序。因此,Twill 分两步操作。我们将首先使用标准的模块化调度基于软件流水线的算法来推导出目标循环的初始调度,该调度具有最优吞吐量。然后,从这个初始调度开始,我们将定义一系列 SMT 约束,描述一个联合的软件流水线和 Warp 特化策略,以实现所需的吞吐量,并尝试解决它以找到一个好的解决方案。如果我们找不到一个令人满意的解决方案,我们将用一个更宽松的调度重复这个过程,直到找到一个令人满意的分配。一旦我们这样做,我们将得到一个结果流水线程序。这个流水线具有在特定 GPU 的架构约束和我们的机器模型下可能达到的最佳吞吐量。
>> 因此,我们的方法已经成功地为多代 GPU 架构 Hopper 和 Blackwell 生成了最佳的软件流水线 Warp 特化策略,并且是我们所知的唯一能够做到这一点的系统。
>> 你有一个问题。
>> >> 是的。你怎么知道它是最优的?
>> >> 所以,这里最优是指我们有一个特定的机器模型,说明这个 SM 或处理器实际上能运行指令的速度有多快。因此,我们可以证明它是最优的,通过说这是该特定 SM 可以运行给定计算的最大吞吐量。因此,当你从最优软件流水线开始时,这只是一个启发式方法,因为你假设这是一个添加 Warp 特化策略的好地方。
>> >> 不,它不是启发式方法。它精确地是软件流水线,初始软件流水线状态表明这个循环在这个硬件上能运行的最佳速度是,我们可以每三个时钟周期完成一个指令,或者抱歉,我们可以每三个时钟周期完成一次迭代,而这只是一个下限。然后,我们问的是,现在我们能否找到一个联合调度和 Warp 特化策略,仍然可以实现每三个时钟周期完成一次迭代的吞吐量。
>> >> 我明白了。所以这就像一个数字综合风格,你正在说,是否存在一个解决方案可以实现最优的软件流水线?如果答案是否定的,那么它是否实现了最优软件流水线的一个以内?你可以稍微退一步。
>> >> 没错。
>> >> >> 也许,抱歉打断一下,也许我听错了。这是否也意味着,这实际上是硬件的光速?关于给定特定模型硬件的速度,比如我知道我的张量核心可以在五周期内完成一个 GEM,我知道完成一个符号需要三个周期,就像在那个模型下,在那个模型下,硬件就像这个。
>> >> >> 和,哦,我明白了,但你说的是,你对调度是最优的,而不是。
>> >> >> >> 我们也实现了这些调度,在现实生活中,并且看到了良好的性能,但它就像,它是最优的,给定一个模型,这台机器可以同时做 X 数量的事情,以这些速率,没有更好的。
>> >> >> >> 所以,稍微简化一下。
>> >> >> >> >> 好的。
>> >> >> >> >> 那么,让我们看看它是如何工作的。因此,当我们开始时,我们将接收。
你知道我们要运行的程序。哦,请继续。我应该回到上一张幻灯片吗?>> 是的,就一张,上一张幻灯片。那么,嗯,在多大程度上,硬件模型是由一个特定的程序或计划设计的,并且受到它的启发?鉴于这一切都是内部完成的,似乎有机会这样做。嗯,我不认为,我认为建模更多的是针对特定的 GPU,只是硬件提供了一系列功能单元,而这些功能单元具有吞吐量,我认为也许你问题的答案是趋势是获得更好的性能,我们正朝着这样的世界发展,即这些功能单元越来越可预测,硬件正在做硬件,软件的变异性越来越小,而硬件就像,你知道,我会以这个速率为你做一个 gem,我也会为你做一个 x,所以这就是,你知道,这就是正在被利用的。>> 是的,谢谢。>> 好的,所以我们输入我们的程序,正如我所说,我们将应用,你知道,现成的软件流水线算法来获得初始计划,这个计划,你知道,基于软件流水线算法说这是我们可以做的最好的事情。所以我们把它拿来,我们展开这个程序一次。所以我们想象运行这个程序,其中主循环只运行一次,我们将得到一个特定的直线代码片段,然后从这个直线代码中,我们将列出关于何时特定计划或指令排序形成有效软件流水线的约束集,然后我们将列出关于给定特定 GPU 的架构特征,特定 warp 专业化策略或这些指令到 warp 的分配是否有效的约束。然后,在这些约束的联合下,我们将求解一个实现最小执行时间的计划和 warp 分配策略。所以让我们看看这些约束实际上是什么样的。所以对于软件流水线,我们将把它分解为一个离散调度问题,我们试图将单个指令映射到它们需要运行的具体周期数和资源上。我们将通过一个布尔变量的多维数组来跟踪这一点,该数组跟踪对于特定指令,该指令的出现,它是否在特定的时钟周期被调度?当我们这样做时,我们就可以写下一堆约束。特别是,这个约束捕获了指令之间的数据依赖性,例如,如果我在这两个 S0 和 P 0 操作之间存在数据依赖性,并且我说我的机器模型说 gem 需要两个周期。我不能在第一个周期调度后续指令,因为数据还没有准备好。然后对于我的 warp 专业化约束,它们将在我将这些指令映射到不同的 warp 时捕获。我们再次通过一系列布尔变量来捕获这些,以确定特定指令是否应该放在特定 warp 上。然后我可以写下许多 warp 专业化约束。这个特定的约束捕获了任何 warp 上可用数据的量。它表明每个 warp 在所有时钟周期上的实时数据总量需要低于架构限制。而且很酷的是,这些 SK,这些约束开始相互作用,实际上这个活跃度谓词是根据上一张幻灯片中的调度谓词定义的,允许求解器做出这样的决定:好吧,我可以并行运行更多操作并增加寄存器预算,或者我可以将它们分成不同的 warp,或者我可以不并行运行它们并降低资源利用率。所以一旦我将所有这些联合起来求解,我就会得到一个计划,它告诉我何时应该运行这些指令,以及一个 warp 分配,告诉我应该在哪里运行这些指令。我将它们重新组合起来,并将这个程序重新展开成具有相应策略的流水线循环,并且我可以证明它在机器上实现了最佳吞吐量。所以我们在一些实际程序上运行了这个程序,包括 flash attention 程序。并且我们发现了与人类专家经过一段时间的努力最终达到的策略完全相同的策略。所以这些是正在运行的循环图的数据流表示。这里的每组颜色都是负责运行这些指令子集的 warp。所以其中一些策略是相同的,这些绿色操作实际上是数据加载操作,并且它们在两代架构的 warp 上都放置在相同的 warp 上。但随后情况迅速发生变化,负责运行自然张量核心指令的 warp 在代际之间发生了变化。然后负责执行不同类型的非矩阵乘法算术的 warp 在代际之间也发生了变化。所以,如果没有像 Twill 这样的系统来以结构化的方式枚举空间并向你证明,你知道它得到的东西实际上是好的,那么就很难决定为什么一种策略实际上比另一种更好。>> 是的,问题。>> 嗯,据我所知,软件流水线和 warp 专业化都纯粹属于调度领域,所以没有你,没有使用,你知道,用其他计算替换某些计算,或者>> 没有代数变换等等。>> 是的。>> 是的。Y>> 这个优化对于系统引入的变异性有多鲁棒性,例如,哪个调度程序可以重新排序指令的调度方式?>> 是的,我的意思是,我们没有对那种事情进行建模,我的意思是,我们主要从设备上的光速吞吐量进行建模,你知道,它可以每周期进行一定数量的乘法,一定数量的矩阵乘法等等,这就是,你知道,我们建模的方式,我们没有对可能干扰你的对抗性 warp 调度程序进行建模。但我会在下一张幻灯片中向你展示,尽管存在潜在的硬件变化,我们仍然表现良好。>> 是的。>> 求解器需要多长时间才能找到这个?>> 是的,这两者都在一分钟之内。我认为左边的那个大约是 20 秒,右边的那个大约是 30 秒。>> 问题有多大?比如有多少变量,有多少约束?>> 我不知道我能否立刻告诉你。我的意思是,我认为我们创建了数千个变量和约束,然后将其交给求解器。嗯,>> 你在使用哪个求解器?>> Yikes 是这个问题的最佳求解器。Yikes 2>> 比所有其他求解器都表现得更好,但我对求解器如何运行没有直观的认识,因为同样的问题在这里可能需要 CBC5 花费 30 分钟,但你知道,这并不意味着一个问题比另一个问题实际上更难。>> 是的,>> 完全吹毛求疵。为什么 Blackwell 上有东西减去自身?>> 那只是一个幻灯片吗?>> 你在哪里将其归零?>> 我明白了。这只是零化的更便宜的方式,因为尺寸可能很大。我>> 我不知道。我明白了。>> 嗯,它可能,所以这里的循环可能指的是迭代之间的依赖关系,比如我可能需要前一次迭代中的值并减去它来得到下一个值。所以这就是循环可以出现的地方。好的。好的。是的。所以,正如我所说,我们在实际硬件上实现了这些,并且你确信工具生成的策略可以实现,你知道,与手动实现的策略相比具有竞争力,比如在 KUDNN 或 flash engine 4 中,并且它可以比基于启发式的方法表现得更好,比如在 Triton 编译器中,所以在这部分演讲中,我谈到了在这些加速器上构建高性能软件的一些可移植性挑战,尤其是在它们随时间变化的情况下。是的,>> 抱歉耽误你了。你能加上,你知道峰值是多少吗?>> 是的。所以对于这个特定的配置,嗯,我不想深入细节,但在这个配置中,你运行张量核心,峰值大约是 1250 或 1300 teraflops。>> 嗯,所以我们非常接近峰值。是的。>> 嗯。因为基本上计划告诉你,你一直在运行张量核心。所以我们期望在最终结果中看到这一点。好的。所以,我向你展示了 warp 专业化和软件流水线这两个优化对于性能非常重要,并且我展示了即使它们的策略随时间变化,我们也可以使用一种统一的基于约束的表示来解决它,以获得机器可移植的策略,并且在我的博士论文的更大工作中,你知道,我研究了这些系统,从顶层来看,为最终用户构建高性能的分布式库,构建分布式运行时系统技术来实际支持这些高层库,然后构建编程系统来实际开发跨这些机器以及在单个进程上运行的内核。所以展望未来,我认为我在这个领域的工作经验让我相信,应对我们底层硬件和加速硬件的变化轴是一个关键问题。所以我们的硬件正在扩大规模,正如我在演讲开始时所说的那样,并且它也在随着时间的推移而扩展,因为我们正在构建更复杂、更强大的架构,它们也变得越来越专业化,并且越来越难编程。然后这里还有另一个维度是多样性,即我们正在构建许多不同的供应商正在构建类似的加速器,然后还有来自学术界的加速器进入工业界,比如数据流系统,所以这里有大量的机器组合,我们需要支持,并且我们需要有编程系统来实际允许我们构建嗯,在这些系统上构建高性能软件。所以大量的组合和单个组件的复杂性不断增加意味着编程这个硬件的复杂性正在迅速增加。而且有一些领域,比如机器学习,我们可以投入更多的钱来尝试跟上。而且我们仍然落后。就像我在英伟达这样的公司亲眼所见,一款新芯片发布了,然后成群的内核工程师去为每一代新芯片重写软件。而且有一些领域没有这么多钱,比如科学计算,我们已经完全落后了。比如有许多科学应用程序还没有移植到 GPU 上,而 GPU 已经存在了 15 年。所以我认为这是一个领域,这是一个编程系统领域,可以帮助跟上这种蓬勃发展的复杂性。而且,我认为编程社区和我想要在未来解决的几个重要问题。所以第一个是关于可组合性,正如我在演讲开始时所讨论的。所以可组合性通常与获得最佳性能相冲突,在顺序编程世界中,我们有大型库生态系统,我们可以将它们组合起来并获得相当不错的性能软件。但在并行和加速世界中,我们很少发现这些生态系统,除了我们在 legate legate 生态系统上的初步工作。但我们确实需要构建这样的可组合软件,特别是对于高层用户,这样我们就可以继续构建高效的软件,而不是每次我们有一个新的应用程序想要运行时都从头开始重建一切。第二个是关于抽象,我们需要继续构建编程系统,这些系统可以抽象出这些低级的特定机器的复杂细节,因为即使是专家编写针对这些机器的程序也是一项挑战,而对于大多数程序员来说,这几乎是不可能的,所以我们需要继续构建这些系统,这样我们就可以更快地构建软件,而无需担心这些特定机器的细节并陷入其中。最后一个挑战是关于可移植性,我们需要构建库和编译系统,允许我们在跨代际或不同供应商之间更改最少的代码。我们也正在改变最少的代码,你知道,从单个节点系统到多节点多 GPU 系统。所以我认为这个领域的发展存在一个正反馈循环。所以我们构建了更具可组合性、更具可移植性的软件,我们将提高抽象级别,并允许更多用户针对这些机器。所以我只有一点时间。我想稍微谈谈一些我感兴趣的可移植性领域的具体问题。我的意思是,关于单个加速器,我认为我们今天构建的编程系统非常擅长处理我们围绕预定义轴构建的编译系统的底层硬件的变化。但当我们的硬件给我们带来麻烦,并增加了一个全新的维度,我们需要对其进行优化,比如现代张量核心暴露的软件异步性,你知道,我们的编程系统跟不上,它们通常将大部分优化负担推给了用户,而没有真正做正确的事情。所以有很多方向可以走,比如构建编译器生成器,它们可以从硬件描述中构建整个编译器或编译器的部分,或者构建可移植的优化框架,比如 Twill,它本质上将具有弹性以应对硬件变化,或者构建更通用的程序表示,比如 Cypress 项目,我可以谈谈它,它从根本上捕获了异步性等内容。然后当你从规模上看架构的变化时,我认为分布式系统编程和加速器编程之间的界限正在真正模糊。复杂的架构看起来确实像一个分布式系统,你有多个 diese 通过不同的互连器通信,讨论不同的分布式 HPM 堆栈。整个系统试图伪装成一个单一的处理器,但它实际上是一个分布式系统。然后网络硬件的进步正在将分布式系统编程的挑战推向单个内核编程,现在高性能内核正在通过网络发送单个消息,同时在 GPU 上重叠单个指令。所以我认为有机会构建统一的编程模型,它们可以在整个堆栈中以一个地方谈论单个程序,从而允许我们在所有这些不同的层上进行优化和自动化。所以这将是我的结论,今天我告诉你了我目前的工作和未来的工作计划,这些计划是关于构建编程系统,使这些复杂的高性能机器更容易编程。所以我很乐意回答问题。[掌声][清嗓子]所以,嗯,你的优化依赖于你只展开一次循环的假设。你能多告诉我们一点关于[清嗓子]这个假设吗?嗯,展开一次循环足够吗,还是你设想需要展开 n 次的情况?>> 是的。[清嗓子]所以后续问题可能是,嗯,也许这是已知的,但我不知道。你优化过的循环是否总是可以重新展开,或者优化器使其无法重新展开?>> 是的。所以这是软件流水线的一般属性,我们正在利用它,软件流水线的目标是,你为任何一个迭代构建一个计划,基本上你将这个计划重叠在自己身上几次,并且实际上有一个公式,你知道如何做到这一点,它为你产生一个稳态,保证是,你知道,大部分时间都花在稳态上,其中每个迭代在某个点之后看起来都完全相同,所以我可以,也许我回到这里,嗯,所以[清嗓子]这里是中间循环的稳态。所以一旦你运行了这部分,称为序言,之后每个迭代都会执行稳态,并且它每次看起来都完全相同。所以它既足够,而且它既足够又精确,我忘了另一个词来实际展开一次,因为一旦你有了,如果你执行了这整段代码一次,你就捕获了管道的启动,管道的稳态和管道的排出。所以一次展开是既健全又精确的。所以,因此,我们实际上总是可以重新展开它,因为我们知道如何从单个迭代的特定计划中计算出稳态。是的,>> 你在谈论所谓的正交的或出乎意料的挑战,以及在分布式环境下的工作。我很好奇,据我所知,这些事情大部分都是在相当可靠的环境中完成的。嗯,那么如果消息损坏、丢失、重复、失败、重排序、节点宕机的比率很高,会发生什么?嗯,这是我们应该关注的未来吗?[清嗓子]以及你的愿景如何契合?我个人在这方面的弹性、弹性方面没有做太多工作。我明白,特别是当我们扩展规模,你知道,你有足够多的处理器时,你知道,任何一个处理器在任何时候都可能失败的概率的数学计算,你知道,在某个规模上会增加。所以我认为这是现实,对于那些真正进行大规模大规模训练的人来说,我认为他们有工作要做。我认为尚不清楚你想做出什么样的权衡。例如,为了应对某些类型的故障而变得有弹性是否值得,如果这意味着你的程序的其余部分运行得慢得多?在这种情况下,检查点是否足够,或者,你知道,因为根据我们的经验,我认为当你试图向特定的编程模型添加弹性功能时,即使在没有任何东西失败的情况下,它也会带来显著的成本。所以我认为那里有权衡,我认为你真的想为你的应用程序量身定制,并且你知道,在承诺在你的编程系统中实现这样的事情之前,真正了解你对特定模型的故障率。因为我认为总的来说,我认为这些方法可能太慢了,也许通过应用程序知识,你知道,你可能能够比通用系统做得更好。>> 是的。嗯,[清嗓子]所以你提到硬件和支持实现高效程序的编程系统之间存在一种滞后差距。你是否将这种协同优化软件流水线和 warp 调度的特定解决方案视为一种权宜之计,即在当前的编程模型下,我们如何获得最佳解决方案,以便我们需要一个更好的编程模型,或者你认为这是一种我们可以应用于未来的通用技术?[清嗓子]我更倾向于后者,我认为这是,当我们,所以理想情况下,我没有展示完整的程序,但你可以想象,比如有一个 triton 程序实现了 flash attention,在一个理想的可移植的世界里,同一个程序在每一代架构上都能正常工作,而现在它不能,因为编译器不够智能,无法在每一代新硬件上正确地降低这个程序,而像 tool 这样的系统是这样的编译器可能需要的一个组件,以便在每一代新硬件上继续提供最佳性能,而且我特别喜欢基于 tile 的模型,我认为它们是人们思考这些架构的好抽象,而且我认为,你知道,一个研究领域就是,你知道,能否让这些模型在我们构建的加速器上继续表现良好,因为现在它们实际上不是,每次出现新模型时,都要让编译器正常工作需要大量工作。>> 正在开发这些架构的人,对吧?他们对这将加速计算有很好的直觉,然而,在你的工作之前,他们需要一年的时间来安排它们,你知道吗?比如,他们只是让他们太复杂了吗?只有当你进入分布式领域时,你知道,你知道,他们擅长他们的工作,对吧?他们没有只是增加更多的 alus 并认为我们会获得一些性能,或者他们没有只是扩大张量,他们有吗?对吧?我的意思是,他们做了一些事情来让这个问题变得比我认为它应该的更难。>> 嗯,[清嗓子]所以我想说的是,我的意思是,我实际上无法具体说明硬件设计者在做什么,因为我不知道他们在做什么。但是,但是,我的意思是,也许从机器的数学建模来看,你会认为这已经足够了,但我认为总的趋势是,像这样的问题通常是由硬件处理的,比如在旧一代的 GP 上,你不需要这样做,因为也许 gem 和其他东西之间的延迟差异足够大,以至于这并不重要,或者也许是因为,嗯,也许是因为旧一代的指令足够精细,例如,每个指令只执行 4x4 矩阵乘法。所以,将这些指令与单个循环中的所有其他指令交错是很好的。你知道,你的编译器,比如 NBCC 或 PTX,可以为你做到这一点。所以它对程序员来说通常是自动的。但是,我的意思是,现在的趋势是,你知道,这些矩阵乘法器非常大。它们是 256x256。这些指令的语义对编译器来说也很复杂,你知道,它不仅仅是,你知道,举个具体的例子,旧的张量指令都在寄存器中。所以标准的 LVM 风格分析足以,你知道,进行你想要的所有优化。但是这些指令,比如,它们读取和写入专门内存的任意部分。嗯,它们有副作用。它们需要多个线程来协调使用。所以,你知道,正常的编译技术对你帮助不大。所以越来越多的负担落在软件上,你知道,如何以正确的方式使用这些指令。所以这就是一个普遍趋势。我的意思是,我认为架构师意识到了这一点,但是他们,你知道,软件会适应,代价是,你知道,芯片上的东西不多,你知道,为张量核心提供更多的 LUS。>> 是的。>> 我正在尝试将这个与基于活动的基础模型统一起来。如果你的循环中有某种通信,或者在循环之外,但无论哪种方式,你认为像 12 这样的技术也可以协同优化网络吞吐量和高利用率吗?>> 所以,我的意思是,绝对[清嗓子]我认为框架本身非常抽象,而且,你知道,我们只是,我们不关心指令实际做什么,我们只需要知道它需要多长时间。我的意思是,做这样的事情的主要困难在于,我们需要关于这些指令需要多长时间的静态信息,然后,你知道,当你开始谈论网络时,特别是交换网络,并且你知道,上面有拥塞,预测这些东西需要多长时间就变得更加困难,而且基于求解器的调度方法,特别是使用时间离散化。很难处理可变延迟和类似的东西。但这是我们目前正在研究的东西。但是你可以想象,如果你能够以某种方式限制这些网络操作所需的时间,你就可以找到一个计划,将它们与设备上的其他计算重叠,这基本上是今天的内核人们所写的。嗯,>> 是的,请继续。>> 关于这一点的一个后续问题是,那么,我的意思是,它变成了一个,另一部分是,如果你有一个发送者和一个接收者,你也可以协同优化这些计划。>> 嗯,但然后你必须优化两个潜在的独立内核。这基本上是异构计算的普遍问题,我想,你有什么关于如何使其扩展和组合的想法,还是必须是不同的抽象层?>> 是的。嗯,所以我不确定那个具体问题,但是,我的意思是,我想说的是,你知道,你一次想看到多少程序来进行优化,这是我们在之前的工作中研究过的问题,你知道,例如,这个 actor 任务的东西,你知道,我们实际上必须非常小心地选择如何将这些图分片到多个节点上,以确保你不想看到的是,一个节点实际上描述了 128 个节点将要做的工作量,而你突然发现你的程序内存不足,或者你花了很长时间来编译这个特定的东西。所以,我认为在某个时候,你知道,有一个权衡,你看到的越多,通常你做得越好,但然后你必须为此付出代价,那就是你花费的优化时间。而且我认为你必须在某个地方停止,在那里你会说,它不值得我获得的收益,因为我将花费太长时间来思考它。所以,我不知道那个权衡在哪里。你知道,对于一些系统来说,这可能是值得的。你知道,你会运行代码足够长的时间,以至于花费所有时间来做这件事是值得的,但对于其他系统来说,它就不值得了。好的,酷。是的,>> 谢谢大家。谢谢提问。[掌声]