
在现代计算领域,图形处理单元(GPU)已从专门的图形硬件演变为并行处理的主力,为从尖端视频游戏到革命性科学发现的各种应用提供动力。然而,其巨大的能力并非唾手可得;它需要通过一种复杂而优雅的协调之舞——即 GPU 调度——来解锁。理解 GPU 如何编排数千个并发线程——这是一种与传统 CPU 截然不同的方法——对于发挥其全部潜力并避免微妙的性能陷阱至关重要。
本文将揭开 GPU 调度世界的神秘面纱。我们将首先深入探讨主导执行的核心硬件和软件原理,在“原理与机制”一节中探索 SIMT 模型、线程束分化以及延迟隐藏的艺术。随后,在“应用与跨学科联系”一节中,我们将看到这些概念如何被付诸实践,考察它们如何被应用于优化性能、构建复杂的系统级工作流,以及推动从云计算到实时系统等领域的进步。这段旅程始于机器的核心,从那些让 GPU 得以指挥其庞大并行交响乐团的基本原理开始。
要理解图形处理单元(GPU)如何以惊人的速度处理数千个任务,我们不能将其仅仅看作是中央处理单元(CPU)的更快版本。它是一种本质上完全不同的猛兽,诞生于不同的哲学。CPU 是一位大师级工匠,一位能够以惊人速度和敏捷性执行任何复杂任务的专家。而 GPU 则是一位驾驭众多的宗师,一位指挥着庞大交响乐团的指挥家。它的天才之处不在于快速完成一件事,而在于同时完成数千件简单的事。本章将带领我们深入这个交响乐团的核心,去理解支配其每一个音符的原理。
想象一下,你有一大片庄稼地,需要给每一株植物浇水。你可以雇一个速度超快的园丁,从一株植物飞奔到另一株;或者,你可以组织一支园丁大军,给他们一个简单统一的命令——“向前一步,浇水”——然后在浇灌一排植物的时间内,把整片地都浇完。GPU 正是建立在第二种理念之上,这一原则被称为数据并行:将相同的操作同时应用于许多不同的数据片段。
这种理念最直接的硬件体现是 SIMD(单指令多数据)。想象一位教官对整个排的士兵大喊一个命令。一个单独的指令解码单元获取一条指令,然后由数十个算术单元以锁步方式执行该指令,每个单元处理自己的数据片段。这种方式效率极高。
然而,为这种刚性硬件直接编程可能很繁琐。如果一个园丁遇到了一块石头,需要向旁边迈一步,而其他人则继续向前怎么办?现代 GPU 采用了一种更优雅的抽象,称为 SIMT(单指令多线程)。SIMT 是一种巧妙的障眼法。它为程序员提供了一个熟悉的模型:你编写一个名为内核 (kernel) 的单一程序,就像为单个线程编写一样。但是当你启动这个内核时,GPU 硬件会创建数千个这样的线程,并将它们分组为称为线程束 (warps) 的“排”(通常包含 32 个线程)。每个线程束被一同调度到硬件上,其内的所有线程都执行该程序。其神奇之处在于,虽然你感觉像是在为许多独立的线程编程,但底层硬件仍然在使用其类似 SIMD 的机制,让一个线程束中的所有 32 个线程在同一时间执行相同的指令。这让你在享受多线程编程便利性的同时,获得了 SIMD 的原始效率。
但是,当一个线程束内的线程在路径上遇到分叉时会发生什么呢?考虑一个简单的 if-else 语句。在我们这个 32 个线程的“排”里,可能有 10 个线程满足 if 条件,另外 22 个满足 else 条件。它们无法再执行相同的指令了。这是 SIMT 世界中的一个根本性挑战,被称为线程束分化 (warp divergence)。
硬件的解决方案很简单,但对性能有着深远的影响:它将路径串行化。首先,走 if 路径的 10 个线程被标记为活动状态,而另外 22 个走 else 路径的线程被暂时置于休眠状态(一种称为“被屏蔽”的状态)。线程束执行 if 块中的所有指令。完成后,角色互换:10 个 if 线程进入休眠,22 个 else 线程被唤醒,然后线程束执行 else 块。线程束所花费的总时间是执行 if 路径的时间加上执行 else 路径的时间之和。线程束内部的并行性被暂时打破了。
但是这些分化的路径在哪里重新汇合呢?那些休眠的线程在何处醒来,并与它们的同伴重新加入锁步执行?这并非一个随意的点。它在程序的控制流图中是一个精确的位置,由一个来自计算机科学的优美概念定义:直接后支配节点 (immediate postdominator)。在程序图中,如果从分支点(比如 )到程序出口的每一条可能路径都必须经过某个节点(比如点 ),那么我们称该节点 后支配分支点 。直接后支配节点就是第一个这样的强制汇合点。硬件被设计为在这个精确、被正式定义的点上自动地使分化的线程重新汇合。
这个机制虽然优雅,但却可能给粗心的程序员设下致命陷阱。想象一下,一个线程束中的线程试图获取一个自旋锁(一种常见的同步工具)。只有一个线程,我们称之为线程 7,可能赢得竞争并获取锁。它进入代码的“临界区”,而其他 31 个线程则被分流到一个自旋等待循环中。现在,如果临界区包含一个屏障 (barrier),即一条指令说“在此等待,直到线程束中所有 32 个线程都到达”?线程 7 到达屏障并乖乖地等待它的 31 个同伴。但那 31 个同伴却卡在自旋等待循环中,等待线程 7 释放锁。而线程 7 因为卡在屏障处而无法释放锁。这是一个完美的、无法打破的死锁,源于一个标准的同步原语与 SIMT 分化现实之间的微妙相互作用。
每个处理器都必须应对延迟——从内存中获取数据时不可避免的延迟。现代 CPU,这位大师级工匠,通过蛮力与智慧来解决这个问题。它拥有巨大的缓存来使数据更靠近,还有一个复杂的乱序执行引擎,可以预读程序,找到独立的指令,并在等待一个漫长的内存加载时执行它们。它是一位减少延迟的大师。
GPU 采取了一种完全不同的、近乎禅宗的方式。它不与延迟作斗争,而是接受它并将其隐藏起来。这就是隐藏延迟的艺术。当一个线程束发出一个需要数百个周期才能完成的命令(如内存读取)时,调度器不会让整个处理器停顿。相反,它会说:“好的,你去等吧。下一个!”然后在下一个周期,它会换入一个完全不同且已准备好执行的线程束。当第一个线程束的数据最终从内存到达时,它会被再次标记为“就绪”,并获得下一次运行的机会。
为了让这个技巧奏效,调度器需要一个庞大的就绪线程束池可供选择。衡量这个池子有多满的指标,称为占用率 (occupancy)。占用率是指一个流式多处理器(SM)上活动线程束的数量与硬件所能支持的最大数量之比。如果你的占用率高,SM 就像一个繁忙的工厂,有很多工人;如果一个工人需要等待材料,另一个工人会立刻在装配线上接替他的位置,工厂的产出保持高水平。如果你的占用率低,就像只有一个工人;当他休息时,整个工厂都停工了。
是什么限制了占用率?SM 的资源是有限的。你运行的每个线程都需要在寄存器中存储其变量,每个线程块可能需要一片高速的片上共享内存。如果你的内核很“贪婪”,每个线程使用大量寄存器,或者每个线程块使用大量共享内存,那么你就无法在 SM 的硬件上容纳同样多的线程束。限制因素——无论是寄存器、共享内存还是最大线程数——决定了可以驻留多少个线程块,从而决定了可以驻留多少个线程束,这反过来又设定了你的占用率。程序员选择每个线程使用 64 个寄存器而不是 32 个,这看似微小的选择,却可能将最大驻留线程块数量减半,从而削弱 GPU 隐藏延迟的能力。
从单个 SM 内部纳秒级的决策中抽身出来,我们会看到另一层调度:系统级调度。现代 GPU 通常是一种共享资源,既要运行像渲染视频游戏帧这样的高优先级、延迟敏感型作业,也要运行像训练神经网络这样长时间运行、吞吐量导向的计算作业。系统如何指挥这个多样化的交响乐团呢?
考虑一个简单的非抢占式策略:一个内核一旦开始,就一直运行到完成。现在,想象一个需要 毫秒的长时间计算内核正在运行。在时间 时,一个游戏的第一帧到达,它需要 毫秒来渲染,并且必须在 毫秒前完成。然而,GPU 正忙。这个帧的工作负载必须等待。到计算内核在 毫秒时完成时,游戏帧已经无可挽回地迟到了。这种级联延迟可能导致后续的每一帧都错过其截止时间,从而破坏用户体验。
显而易见的解决方案似乎是抢占:给予图形工作更高的优先级,并允许它中断计算内核。当帧到达时,系统保存计算内核的状态,运行图形工作,然后恢复计算内核。这很有效!即使每次上下文切换有少量开销,帧现在也能按时完成。
但故事还更微妙。如果我们使用一个带有严格优先级的多级队列(MLQ)调度器(图形总是在计算之前),但底层硬件仍然是非抢占式的呢?有人可能认为优先级就足够了。并非如此。想象一下,图形内核在 毫秒的帧预算中需要 毫秒,留下 毫秒的“空闲时间”。一个低优先级的计算内核,需要 毫秒,可能会在这个空闲时段的后期被分派。如果它在时间 开始,它将运行到 。但下一个高优先级的图形帧在 到达!它被这个“不重要”的计算内核阻塞了 毫秒。这个小小的延迟打乱了下一帧的时间,导致下一帧遭受不同的延迟。结果不是一个恒定的延迟,而是帧开始时间出现一种奇特而优美的周期性振荡,一个抖动模式,在重复之前会循环经历 毫秒的延迟。这揭示了一个深刻的真理:在调度中,没有抢占的严格优先级是一个不完整的解决方案。
为了真正驾驭系统,现代调度器会混合使用多种策略。它们使用操作系统设置的外部优先级来识别对延迟敏感的工作。它们可能会使用空间分区,专门为高优先级任务保留几个 SM,创建一条永不阻塞的快车道。它们还可以使用时间分区,将长时间运行的计算内核分解成更小的“微内核”。这为协作式让步创造了条件,允许一个长作业周期性地检查是否有更重要的工作到达。这避免了真正硬件抢占的高昂成本,同时仍能确保一个简短、紧急的任务永远不会被一个长任务所阻碍。事实证明,GPU 调度器不是一个指挥家,而是一个指挥家们的层级体系,从系统级到单个线程束协同工作,一切都是为了让音乐持续演奏,不错过任何一个节拍。
理解图形处理单元(GPU)调度的原理是一回事;而亲眼目睹其运作,则如同见证一个宏伟的交响乐团焕发生机。现代 GPU 不是单一的乐器,而是由数千个微小而简单的演奏者——核心——以及负责内存传输、视频解码等任务的专门部门组成的交响乐团。如果任其自然,它们只会产生噪音。因此,GPU 计算的艺术,就是指挥这个交响乐团的艺术。调度器是总指挥,而它的乐谱就是一套规则和算法,将充满可能性的嘈杂声响转变为计算的杰作。
这段应用之旅将我们从单个乐句的微观调优,带到全球云服务和前沿科学发现的宏大编排。我们将看到,关于管理工作、资源和依赖关系的相同基本思想如何在每个尺度上回响,揭示出并行计算世界中非凡的统一性。
在其核心,GPU 是为速度而生的。调度最直接的应用就是释放这种原始潜力,使计算尽可能快地运行,达到物理和硅晶片 법칙所允许的极限。这不是一项蛮干的工作,而是一种精巧的平衡艺术。
想象你是一位试图最大化产量的工厂经理。你有一个工厂车间(一个流式多处理器,即 SM),其空间(共享内存)和工位(寄存器)数量都是固定的。你需要由工人团队(线程块)来组装产品(进行计算)。如果你让团队规模太大,他们可能需要太多的空间来放置零件和工具,以至于整个车间只能容纳一个团队。这使得大部分车间空置,效率低下。如果你让团队规模太小,你可以容纳很多团队,但每个团队都太小,无法有效协作,管理如此多小团队的开销也成了负担。
这正是 GPU 程序员面临的“金发姑娘”问题。为了最大化性能,我们必须选择一个恰到好处的线程块大小。一个关键目标是实现高占用率——让 SM 的处理单元尽可能保持繁忙。这样做的主要原因之一是为了隐藏内存访问不可避免的延迟。当一组线程(一个线程束)等待数据从主内存到达时,调度器可以立即切换到另一个准备好计算的驻留线程束。要有效地做到这一点,它需要一个庞大的就绪线程束池。
调度器创建这个池的能力受到每个线程块所需资源的限制。一个内核可能每个线程需要一定数量的高速片上共享内存,或者大量的寄存器。正如我们在一个假设的调优练习中所见,这里存在一个复杂的权衡:更大的线程块尺寸可能有利于组织工作,但它会消耗更多资源,从而限制了能同时驻留在 SM 上的线程块数量。最优配置是能够最大化 SM 上活动线程束总数的配置,从而为调度器提供尽可能大的任务菜单以隐藏延迟。有时,最重要的资源是共享内存,而最大化性能意味着找到一个能尽可能充分利用 SM 共享内存预算的线程块大小,即使这意味着只有一个或两个线程块驻留。这就是性能调优的微观之舞,是程序员与调度器之间的直接对话。
但如果问题本身不适合并行执行呢?有时,我们不仅要调整乐团,还要重写乐谱。考虑求解大型线性方程组的问题,这是从流体动力学到结构力学的计算科学基石。一种经典方法,逐次超松弛(SOR)法,虽然非常简单,但本质上是串行的。每个点的更新都依赖于其紧邻点的值,而该邻点恰好是在前一步刚刚更新的。并行调度器看到这种情况,会发现一个它无法打破的令人沮丧的依赖链。
解决方案是算法与架构之间一种优美的协同设计:红黑排序。想象一下点的网格是一个棋盘。我们可以将其涂成红色和黑色。关键的洞见是,对于标准的五点模板,一个红点的更新只依赖于它的黑邻居,而一个黑点的更新只依赖于它的红邻居。因此,算法被重新构造:首先,同时更新所有红点——它们都是独立的。然后,一旦它们全部完成,再同时更新所有黑点。这个两步过程打破了串行依赖链,并在每个步骤内创造了大规模的并行性,这非常适合 GPU 调度器。这种改变并非没有代价;它可能会改变该方法的数学收敛特性,并引入必须管理的新内存访问模式。但这是一个深刻的例子,展示了我们如何能够改变算法的根本结构,使其能够与并行调度器“对话”。
一个单一、优化的内核只是宏大叙事的一部分。真实世界的应用是复杂的流水线,涉及数据移动、预处理、计算和后处理。一个高超的调度器不仅要指挥计算核心,还要指挥 GPU 乃至主机 CPU 上所有专门硬件单元的整个合奏团。
想一想现代视频处理流水线。一个压缩的视频帧从主计算机传来,它需要被解码,应用一个滤镜,然后结果必须被发送回去。一种天真的方法是为每一帧逐一执行这些步骤。但现代 GPU 拥有独立的引擎来处理这些任务:用于数据传输的复制引擎、专用的硬件解码器以及用于滤波的计算核心。一个聪明的调度策略使用一种称为流 (streams) 的概念来创建一条流水线。当计算引擎正在为帧 应用滤镜时,解码引擎可以同时处理帧 ,而复制引擎可以从主机传输帧 。这种通过流和同步事件管理的流水线技术,允许 GPU 的所有部分并行工作。整个系统的吞吐量不再是所有阶段持续时间的总和,而仅受限于最慢阶段的持续时间——即瓶颈。
这种重叠工作的原则超越了 GPU 本身,延伸到了 GPU 与主机系统之间的连接。对于机器学习和数据科学中的许多应用来说,数据集太大,无法装入 GPU 内存。这被称为核外 (out-of-core) 处理。数据必须通过 PCIe 总线从主机的内存中流式传输,这比 GPU 自身的内存要慢得多。如果 GPU 必须等待每一批新数据的到来,它将花费大部分时间在空闲上。解决方案是另一个优雅的调度技巧:双缓冲 (double buffering)。调度器在 GPU 内存中分配两个缓冲区。当 GPU 正在处理缓冲区 A 中的数据时,调度器使用复制引擎同时将下一批数据传输到缓冲区 B 中。当 GPU 完成缓冲区 A 的处理后,它立即开始处理缓冲区 B,而调度器则开始将再下一批数据传输到缓冲区 A 中。通过总是领先一步工作,调度器可以有效地隐藏数据传输的延迟,保持强大的计算引擎持续获得数据并保持繁忙。
系统级编排的最终步骤是将所有可用的处理器——包括 CPU 的众多核心和 GPU 的众多核心——视为一个单一的、异构的资源池。不同的任务更适合不同的处理器。一个高度并行但涉及简单操作的任务非常适合 GPU,而一个复杂、多分枝的任务可能更适合 CPU。例如,在视频编码服务中,可以有一个高质量的 GPU 加速路径和一个较低质量的纯 CPU 路径。系统的调度器此时必须扮演交通调度员的角色,决定将传入工作的多大一部分分配给 GPU 路径,多大一部分分配给 CPU 路径。目标是找到一个完美的平衡点,使得 GPU 和 CPU 都不会成为瓶颈。通过动态分配工作负载,调度器可以最大化整个系统的吞吐量,从每一个可用的晶体管中榨取性能。
GPU 调度的应用远远超出了对速度的抽象追求,已融入我们日常生活的方方面面和科学的前沿领域。在这里,调度的目标从单纯的“更快”扩展到包括“更公平”、“更安全”和“更强大”。
你使用个人电脑的体验就是对复杂的实时 GPU 调度的证明。当你移动鼠标时,光标平滑地滑过屏幕。这是由一个名为图形合成器的高优先级任务管理的。它的工作是为显示器上显示的每一帧绘制用户界面(UI),通常每秒 60 次。这是一个硬实时任务:错过截止时间会导致可见的卡顿或“掉帧”,从而破坏用户体验。现在,当你在后台启动一个要求很高的游戏或科学计算时会发生什么?这是一个较低优先级、尽力而为的任务。操作系统调度器面临一个挑战:它必须给予计算任务足够的 GPU 时间以取得进展,但它必须保证合成器总是可以抢占计算任务以满足其截止时间。
由于在 GPU 上切换任务存在开销,调度器不能在任意时刻中断计算任务。它通常使用时间片。关键问题是,一个计算时间片应该多长?如果太长,合成器可能需要等待时间片结束,等到它运行时,就已经错过了截止时间。这被称为阻塞。通过仔细分析,考虑最坏情况下的阻塞时间、切换开销以及合成器自身的执行时间,操作系统设计者可以计算出在保证 UI 流畅的前提下,可能的最长时间片。这种分析甚至必须考虑到一些微妙的影响,比如一个高优先级的 CPU 任务被一个正在使用非抢占式锁向 GPU 提交工作的低优先级任务暂时阻塞的情况。这就是调度如何确保你的计算机即使在繁重工作时也能保持响应。
这种共享单一强大资源的想法是云计算的基础。云服务提供商如何能同时为数千名客户提供 GPU 服务?答案是虚拟化,这是一套用于分割物理 GPU 的技术,使其可以被多个隔离的虚拟机(VM)使用。调度是这一挑战的核心。有几种策略,每种都有不同的权衡。一种是在 VM 中拦截图形命令并为宿主机 GPU 进行“翻译”(API 远程处理),这提供了极大的灵活性但开销很高。或者,可以在软件中完全模拟一个假的 GPU,这提供了完美的隔离但性能很差。对于高性能工作负载,最有前途的方法是利用硬件支持。像单根 I/O 虚拟化(SR-IOV)这样的技术允许一个物理 GPU 暴露出多个“虚拟功能”,每个功能都可以直接传递给一个 VM。调度器于是成为 GPU 自身的一个硬件特性,提供受保护的、低延迟的访问。这允许多个用户在共享的 GPU 上运行未经修改的高性能应用程序,由硬件本身来强制执行公平性和安全性。这就是使得庞大的云端 GPU 集群成为可能的原因。
最后,在科学计算的最前沿,GPU 调度正在演变,以管理前所未有复杂性的工作流。模拟地震、气候变化或蛋白质折叠等现象,涉及的不是一个内核,而是一个由相互作用的物理模块组成的复杂网络。一个力学模拟可能需要流体流动模拟的结果,而后者又依赖于一个损伤模型。这个依赖关系网络可以表示为一个有向无环图(DAG)。现代运行时可以接受这个图,并在一个 GPU 集群上动态调度任务。调度器的工作极其困难:任务持续时间可能无法预测,每次运行都可能不同。通过使用复杂的启发式算法,例如优先处理位于工作流预估“关键路径”上的任务,并使用模拟来理解随机性的影响,这些高级调度器在几年前还无法想象的规模上编排计算任务。
从单个内核的纳秒级到气候模拟的数小时,从游戏玩家的桌面到云数据中心,GPU 调度的原则是一股统一的力量。它是一门控制与协调的艺术,一门平衡相互竞争的需求和隐藏不可避免延迟的艺术。正是这种无形的智能,解锁了并行计算真正惊人的力量,将一群简单的硅片演奏者,转变成一个能够解决世界上一些最具挑战性问题的交响乐团。