Microsecond-scale Preemption for Concurrent GPU-accelerated DNN Inferences

作者:Mingcong Han1,2, Hanze Zhang1,4, Rong Chen1,2, and Haibo Chen1,3
机构:1上海交通大学并行与分布式系统研究所(SEIEE),2上海人工智能实验室,3教育部领域专用操作系统工程研究中心,4上海交通大学人工智能研究院,教育部人工智能重点实验室

A1 主要贡献

许多智能应用(如自动驾驶和虚拟现实)需要在GPU上同时运行延迟关键型(latency-critical)和尽力而为型(best-effort)的DNN推理任务,以实现实时响应和工作保持(work conserving)。然而,商用GPU缺乏高效的抢占式调度支持,现有技术要么独占GPU导致利用率低下,要么让实时任务等待尽力而为任务完成导致高延迟。

本文提出了REEF,这是首个在GPU调度中实现微秒级内核抢占和受控并发执行的GPU加速DNN推理服务系统。REEF的创新体现在两个方面:
1. 基于重置的抢占方案 (Reset-based Preemption):基于DNN推理内核大多是幂等的(idempotent)这一观察,REEF设计了一种基于重置的抢占方案。当实时任务到达时,该方案通过主动终止并随后恢复正在运行的尽力而为内核,从而在微秒级别的时间内将实时内核调度到GPU上。
2. 动态内核填充机制 (Dynamic Kernel Padding):鉴于DNN推理内核具有多样化的并行度和可预测的延迟,REEF提出了一种动态内核填充机制。该机制根据实时内核的资源需求,动态地选择合适的尽力而为内核来“填充”GPU的剩余资源,从而在几乎没有开销的情况下充分利用GPU。

本文的主要贡献如下:
* 深入理解GPU加速DNN推理的特性:揭示了DNN推理任务的幂等性、大量内核、延迟可预测性和并行度多样性等特点,并分析了现有GPU调度方案在应对这些特性时存在的问题(§2)。
* 新颖的基于重置的抢占方案:该方案能够在几微秒内将实时内核调度到GPU上,且抢占延迟与被抢占内核的数量无关(§4)。
* 优雅的动态内核填充机制:该机制能够动态地将尽力而为内核与实时内核结合,以充分利用GPU的大规模并行性(§5)。
* 在AMD和NVIDIA GPU上的实现与评估:通过在一个新的DNN推理服务基准(DISB)和真实世界轨迹上的评估,证明了REEF相比现有技术的优越性和有效性(§6, §7)。


图 1: (a) DNN推理(包括实时和尽力而为任务)的总体吞吐量;(b) 使用并发GPU调度(即多个GPU流)时实时任务的端到端延迟;(c) 使用抢占式GPU调度(即基于等待的抢占)时实时任务的端到端延迟;(d) 随着实时任务频率增加,尽力而为任务的吞吐量。工作负载:VGG(实时)和ResNet(尽力而为)。测试平台:一块拥有16GB内存的AMD Radeon Instinct MI50 GPU。

A3 背景知识、关键观察与设计原则

2.1 GPU加速DNN推理的特性

深度神经网络(DNN)由多个通用层(如卷积层、池化层和全连接层)的实例组成。为了在GPU上服务推理请求,预训练的DNN模型会提前加载到GPU内存中。对于每个到达的请求,DNN模型的所有内核会依次执行,并将结果返回给应用程序。


图 2: 使用类似ResNet模型的DNN推理示例。

DNN推理被用于实时(RT)任务(如障碍物和交通灯识别)和尽力而为(BE)任务(如情绪和疲劳监测)。实时任务对延迟非常敏感,而尽力而为任务没有严格的时间要求。

2.2 现有GPU调度技术

DNN推理服务系统依赖GPU调度来满足两个可能冲突的性能目标:低延迟和工作保持。尽管GPU调度在高性能计算社区已有广泛研究,但DNN推理的独特性质带来了新的挑战。


图 4: 一个混合工作负载的GPU任务调度示例,包含两个尽力而为任务和一个实时DNN推理任务。GPU有四个计算单元(CU)。(a)顺序执行无抢占 (b)块级抢占 (c)多GPU流并发 (d)理想调度

3 REEF 概览

3.1 系统架构

REEF的目标是提供抢占式GPU调度,以实现延迟关键型任务的实时性和尽力而为任务的工作保持性(如图4理想调度所示)。基于DNN推理内核大多是幂等的,并且存在大量具有不同并行度和可预测延迟的内核这一洞察,REEF提出了两个新颖的设计:基于重置的抢占动态内核填充

REEF的架构如图5所示,包括离线和在线两部分:
* DNN模型准备(离线):DNN模型首先被编译和优化,然后加载到模型池中。REEF扩展了模型编译器(如TVM【15, T. Chen et al. TVM: An Automated End-to-End Optimizing Compiler for Deep Learning. In 13th USENIX Symposium on Operating Systems Design and Implementation, OSDI’18, 2018.】),增加了一个代码转换模块,用于验证内核的幂等性并转换源代码以辅助调度。此外,REEF开发了一个内核分析器来测量每个内核的计算需求和执行时间。


图 5: REEF的架构。虚线框中的模块位于服务DNN推理请求的关键路径上。

3.2 一个说明性示例

图6展示了REEF中调度五个DNN推理任务的时间线。
1. REEF收到前两个尽力而为请求r1b1,系统在正常模式下运行,两个任务的内核被调度到两个不同的GPU流上并发执行。
2. 在r1b1执行期间,一个实时请求v1到达。调度器立即切换到实时模式,GPU运行时通过终止所有正在运行的尽力而为内核(即r1b1)来立即抢占GPU。
3. 同时,DKP模块从被恢复的任务中选择合适的内核来动态填充实时任务v1的内核。之后,填充后的内核将在GPU上单独执行。
4. v1完成后,调度器切换回正常模式。所有正在运行的和后续的尽力而为任务(即r1, b1, b2, r2)将通过两个GPU流在GPU上并发执行。


图 6: REEF中的时间线示例。这里的DNN推理任务与图4中的相同。

A2 方法细节

4 基于重置的抢占 (Reset-based Preemption)

核心思想。我们的核心思想,即基于重置的抢占,是利用DNN模型中GPU内核大多是幂等的特性,这使得主动抢占成为可能——立即终止GPU上所有正在运行的内核,并在稍后恢复它们。这样做的好处有两点:首先,它避免了保存和恢复GPU庞大的上下文(例如每个CU有256 KB的寄存器文件)【70, I. Tanasic et al. Enabling Preemptive Multiprogramming on GPUs. ACM/IEEE 41st International Symposium on Computer Architecture, 2014.】。其次,无需等待所有正在运行的内核完成,这可能需要数百微秒的时间。

面临的挑战。然而,在商用GPU上实现基于重置的抢占仍然面临新的挑战。除了在GPU上运行的内核外,还有数百个已启动的内核被缓存在GPU运行时维护的多个队列中。这对于隐藏内核启动时间并充分利用GPU的大规模并行性是必要的。但是,驱逐所有已启动的内核使得在几十微秒内抢占GPU变得异常困难。

内核生命周期。图7说明了已启动内核在GPU运行时和设备中的生命周期。首先,调度器启动一个推理任务的所有内核,并为每个任务指定一个GPU流。GPU运行时为每个GPU流维护一个称为主机队列(host queue)的链表来缓冲已启动的内核。每个主机队列都有一个后台线程,将缓冲的内核异步传输到一个称为设备队列(device queue)的环形缓冲区,该队列可由CPU和GPU同时访问。GPU的命令处理器会轮询所有设备队列以获取缓冲的内核,并最终将它们分派给计算单元。因此,一个推理任务的已启动内核可能存在于三个地方:主机队列(HQs)、设备队列(DQs)和计算单元(CUs)。为了实现即时抢占,必须驱逐这三个地方的所有内核。


图 7: REEF中为实现即时抢占而扩展的GPU运行时。

4.1 驱逐缓冲的内核 (Evicting Buffered Kernels)

主机队列和设备队列的重置。基于重置的方法需要主动从主机队列和设备队列中驱逐所有缓冲的内核。对于主机队列,重置它们很简单(图7中的➊),只需将所有缓冲的内核出队并回收内存,因为它们完全由GPU运行时控制。然而,对于设备队列,GPU运行时无法驱逐缓冲的内核,因为GPU的命令处理器可以直接从设备队列中获取内核【23, ROCm documentation. GCN Native ISA LLVM Code Generator: Kernel Dispatch, 2022.】,这会导致数据竞争和不可预测的结果。CPU也没有提供安全驱逐设备队列内核的方法。一个潜在的解决方案是通知GPU重新注册一个新的设备队列【62, ROCm Core Technology. AMD GPU kernel driver with KFD: unmap_queues_cpsch, 2022.】,但这会带来不可接受的延迟开销(在我们的测试平台上约为1毫秒)。

懒惰驱逐机制。受可驱逐内核【12, Guoyang Chen et al. EffiSha: A Software Framework for Enabling Efficient Preemptive Scheduling of GPU. 22nd ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming, 2017.】的启发,我们提出了懒惰驱逐(lazy eviction)来重置设备队列,而无需扩展GPU运行时和硬件。REEF的代码转换器预先在每个内核的开头注入一段代码,该代码检查一个抢占标志以判断自己是否已被驱逐。当抢占标志为真时,内核将主动终止自己。因此,当抢占发生时,抢占模块会立即在GPU内存中将抢占标志设置为真(见图7中的➋)。设备队列中缓冲的内核将照常被获取并分派到CU,但会立即自我终止。

性能优化。我们最初的队列驱逐机制给抢占过程带来了不可忽略的开销,抢占单个任务需要超过500 µs。深入分析表明,开销主要来自(a)从主机队列回收内存和(b)等待从设备队列获取内核。因此,我们提出了两个优化来减轻这些开销。
* 异步内存回收 (Asynchronous memory reclamation):为了立即从主机队列中驱逐GPU内核,REEF利用一个后台GC线程来异步回收内存。具体来说,REEF通过简单地将头指针置空来重置主机队列,然后通知GC线程在后台回收内存。
* 设备队列容量限制 (Device queue capacity restriction):虽然懒惰驱逐可以立即终止设备队列中的内核,但这些内核仍然需要被获取和分派到CU,每个内核大约需要20 µs。因此,REEF限制设备队列的容量以实现微秒级的内核抢占。调整设备队列容量是在抢占延迟和执行时间之间进行权衡。我们根据经验在测试平台上选择设备队列容量为4,因为它足以在30 µs内重置设备队列,而对正常执行时间的开销可以忽略不计(小于0.3%)。

4.2 终止运行中的内核 (Killing Running Kernels)

利用驱动程序修改。为了避免等待运行中内核的完成,基于重置的抢占会主动终止GPU中正在运行的内核。不幸的是,GPU运行时没有提供API,GPU驱动程序也没有暴露功能来从主机端终止运行中的内核。我们观察到GPU驱动程序有能力终止CPU进程并同时杀死相关的GPU内核。这表明GPU驱动程序确实可以杀死一个未完成的内核。然而,这个功能也会回收进程和GPU内核分配的GPU内存,导致被抢占的内核必须重新加载DNN模型参数,耗时甚至达数秒。

保留内存的内核终止。为了解决这个问题,REEF修改了GPU驱动程序的内核终止功能,并将其暴露给GPU运行时的抢占模块。这个新功能会指示命令处理器杀死CU上所有正在运行的内核,但保留它们在GPU内存中的运行状态。抢占模块将在驱逐主机队列和设备队列后,使用它来杀死所有正在运行的内核(见图7中的➌)。

4.3 恢复被抢占的任务 (Restoring Preempted Tasks)

恢复点的近似。尽力而为的任务在被抢占后应该被恢复。幸运的是,DNN模型中内核的幂等性特性保证了DNN推理任务的执行可以从被中断内核之前的任何一个内核恢复。然而,精确识别被中断的内核几乎是不可能的。

恢复策略。为了解决这个问题,REEF采用了一种近似方法,确保被抢占的任务从至多比中断内核早一个常数(c)个内核的位置恢复。具体来说,抢占模块在开始重置任务队列时,首先记录传输到设备队列的最后一个内核(kl),然后从kl之前的c个内核处恢复被抢占的任务,其中c表示设备队列的容量。我们观察到命令处理器按顺序从设备队列中获取内核并运行它,这意味着中断的内核不会早于设备队列中最后一个内核(kl)之前的c个内核。REEF将冗余执行最多c+1个内核。由于c被配置得相对较小(即4),恢复开销可以忽略不计(约30 µs)。

4.4 在闭源GPU上的抢占

面临的挑战。许多商用GPU(如NVIDIA GPU)是闭源的。这给我们的基于重置的抢占方案带来了新的挑战,必须将GPU运行时视为一个黑盒。主要限制是我们无法重置CU来主动终止正在运行的内核(图7中的➌)。此外,REEF也无法直接在GPU运行时之外操作主机队列和设备队列。

REEF-N方案。我们为闭源GPU提出了一个受限版本的基于重置的抢占,称为REEF-N
1. REEF-N首先将GPU运行时提供的通用抽象——GPU流,包装成一个虚拟主机队列(vHQs),用于拦截和缓冲所有已启动的内核。
2. 每个vHQ也有一个后台线程异步地将缓冲的内核传输到GPU运行时。
3. 之后,REEF-N将整个GPU运行时视为多个设备队列(每个GPU流一个),这样REEF可以轻松地重置vHQs来驱逐缓冲的内核,而不是直接重置物理HQs(图7中的➊)。
4. REEF-N仍然遵循懒惰驱逐来重置DQs,然后等待所有正在运行的内核完成。
5. 最后,为了模拟DQ容量限制,REEF限制了在GPU运行时中未完成内核的数量。


图 8: 使用不同方法并行服务多个内核的示例。

5 动态内核填充 (Dynamic Kernel Padding)

现有方案的局限性。为了实现高吞吐量,实时和尽力而为任务应在GPU上并发执行。然而,现有的方法都不能提供这种受控的并发执行。
* 不同GPU流:使用不同的GPU流启动实时和尽力而为任务无法避免相互干扰。如图8所示,GPU流之间的分派延迟(20–40 µs)可能会推迟实时内核的执行或限制其可用资源。
* 静态内核融合:静态内核融合【74, Guibin Wang et al. Kernel Fusion: An Effective Method for Better Power Efficiency on Multithreaded GPU. IEEE/ACM Int’l Conference on Green Computing and Communications, 2010.】可以在编译时将来自不同任务的多个内核合并成一个。然而,这需要预编译所有可能的内核组合,对于拥有数百个内核的DNN模型来说,这在实践中是不可行的,会导致内核数量爆炸和巨大的内存占用。

我们的方法:动态内核填充。受内核融合的启发,我们的方法也将实时内核和尽力而为内核组合成一个,并使用单个GPU流启动它,如图8所示。不同的是,我们在编译时构建一个模板(称为dkp内核),并在运行时使用函数指针来填充和执行内核。此外,我们动态选择尽力而为内核以避免对实时内核的干扰。

DKP内核实现。图9展示了一个dkp内核的示例。它被声明为一个全局函数(即内核入口)。候选内核函数(如dense)被声明为独立的设备函数,可以作为dkp内核的参数传递,并通过函数指针调用。dkp内核将CU分区,以并行执行一个实时候选内核(rt_kern)和一组尽力而为候选内核(be_kerns)。它首先为实时内核分配足够的CU,然后将剩余的CU分配给尽力而为内核。


图 9: REEF中动态内核填充的伪代码。

5.1 高效的函数指针 (Efficient Function Pointers)

默认函数指针的问题。在GPU上直接使用函数指针会带来两个关键性能问题:
1. 有限的寄存器分配:GPU程序的寄存器数量在编译时确定。由于无法静态确定间接调用函数使用的寄存器数量,GPU编译器会为被调用函数分配一个预定义的静态上限,这可能导致寄存器不足而将变量保存到栈上,从而降低性能。
2. 昂贵的上下文保存:GPU上的间接函数调用比CPU上昂贵得多,因为需要保存和恢复庞大的上下文(例如数十个寄存器)。

REEF的解决方案:代理内核。REEF通过引入全局函数指针作为默认函数指针机制的替代来解决上述问题。由于全局函数被视作内核入口,编译器既不限制其寄存器使用,也不为其添加上下文保存/恢复代码。我们将候选内核声明为全局函数而非设备函数,从而解决了这两个问题。然而,一个全局函数(如dkp内核)不能调用另一个全局函数。为了绕过这个限制,我们在汇编代码中用跳转指令替换间接函数调用,并手动准备候选内核的初始状态。

动态寄存器分配。即使使用了全局函数指针技术,由于过度分配(over-allocation)问题,实时内核的性能仍然不理想。为了满足不同候选内核的寄存器需求,dkp内核必须分配尽可能多的寄存器,这可能会降低CU占用率,从而增加执行时间。REEF通过引入一组代理内核(proxy kernels)来解决动态寄存器分配问题。代理内核与dkp内核共享相同的源代码,但分配不同数量的寄存器,允许调度器根据每个候选内核的寄存器需求动态选择合适的代理内核。

减少代理内核数量。为每个可能的寄存器数量生成代理内核会面临内核数量爆炸的问题。为了减少代理内核的数量,我们为所有可能的CU占用率而不是寄存器数量生成代理内核。由于引入代理内核是为了防止过度分配降低CU占用率,具有不同寄存器数量但共享相同CU占用率的代理内核实际上是冗余的。在我们的测试平台上,这使得代理内核的数量从32,768个减少到10个,而不影响候选内核的性能。

动态共享内存。除了寄存器,共享内存的过度分配也可能降低代理内核的CU占用率。幸运的是,内核可以在启动时通过设置一个属性(即“动态共享内存”)来动态分配共享内存。REEF在模型编译期间将固定大小共享内存的变量声明转换为动态共享内存,从而使代理内核使用的共享内存量可以在运行时根据候选内核的最大需求进行设置。


图 10: 五个DNN模型中内核的实测执行时间。DNN模型的详细信息见表1。

5.2 内核选择 (Kernel Selection)

选择策略。对于动态内核填充,内核选择策略对于避免对实时任务的延迟干扰至关重要。REEF提出了一种贪心启发式方法,以确保尽力而为的块只使用实时内核剩余的GPU资源(即CU)。具体来说,它首先为实时内核保留足够的CU,然后检查尽力而为任务队列,为剩余的CU选择合适的块,直到没有空闲的CU或候选任务为止。被选中的尽力而为块应满足以下两个规则:

策略的保守性。这个内核选择策略高效(选择时间小于1 µs)且有效(实时内核延迟开销平均小于1%),但它也比较保守。例如,当尽力而为内核的执行时间通常比实时内核长时(如图10中的VGG和DenseNet),即使实时任务只使用少量CU,动态内核填充的吞吐量提升也可能微不足道。

6 实现

我们在AMD GPU上实现了REEF,因为它拥有开源平台和ISA。实现基于Apache TVM【73, Apache TVM, 2021.】和AMD ROCm【3, AMD ROCm. AMD ROCm Platform Documentation, 2022.】,大约有5,500行C++代码。此外,为了展示REEF在闭源GPU上的可行性,我们也在NVIDIA GPU上移植了REEF-N,一个受限版本的基于重置的抢占。

A4 实验环境与结果

实验环境

实验结果

总体性能

我们使用DISB工作负载和真实世界轨迹比较了REEF与其他方法(SEQ:顺序执行,GPUStreams:多流并发,RT-Only:仅实时任务)的端到端延迟和总体吞吐量。


图 11: (a) 端到端实时任务延迟 和 (b) 总体吞吐量(包括实时和尽力而为任务)在不同调度方法下的比较。

DNN推理抢占性能

我们比较了REEF的基于重置的抢占和扩展后的基于等待的抢占方法的抢占延迟。


图 12: 基于重置和基于等待方法的抢占延迟比较 (a) 在DISB工作负载上,以及 (b) 抢占不同DNN模型的单个推理任务时。


图 13: 抢占延迟随 (a) 已启动内核数量 和 (b) 内核执行时间的增加而变化。


图 14: (a) 抢占延迟随尽力而为客户端数量增加的变化,以及 (b) 基于重置方法有无优化的延迟分解。


图 15: (a) 抢占延迟和执行时间随设备队列容量的变化,其中[%]显示了正常执行期间的CPU利用率,以及 (b) 不同DNN模型的恢复开销,标签显示了恢复时间(µs)。

动态内核填充性能

我们评估了动态内核填充(DKP)的有效性。


图 16: 使用不同并发执行方案的 (a) 实时任务端到端延迟 和 (b) 总体吞吐量比较。


图 17: 使用动态内核填充,在不同DNN模型组合下运行实时和尽力而为内核的平均CU使用率。


图 18: 使用不同优化的填充内核的 (a) 执行时间开销 和 (b) 内存开销比较。


图 19: (a) DISB A-E的内核选择执行时间 和 (b) 使用动态内核填充的实时内核执行时间开销的CDF。

闭源GPU评估

我们在NVIDIA V100 GPU上评估了REEF-N(受限版本)。


图 20: 在NVIDIA和AMD GPU上使用不同抢占方案对DISB工作负载的抢占延迟比较。

A7 补充细节

8 讨论

9 相关工作

A5 结论

本文介绍了REEF,首个用于商用GPU的DNN推理服务系统,它通过微秒级内核抢占和受控并发执行实现了实时性和工作保持性。REEF的核心创新在于:首先,通过主动终止和恢复尽力而为内核,在微秒级别内为实时内核抢占GPU;其次,通过动态地用合适的尽力而为内核填充实时内核,以极小的开销充分利用GPU。我们还构建了一个新的DNN推理服务基准(DISB)。在AMD和NVIDIA GPU上的评估证实了REEF的有效性和高效性。

A6 附录

本工件提供了REEF的源代码、详细的自述文件以及用于复现OSDI 2022论文“Microsecond-scale Preemption for Concurrent GPU-accelerated DNN Inferences”主要实验结果的脚本。REEF是首个支持微秒级内核抢占和受控并发执行的GPU加速DNN推理服务系统。我们提供了构建软件包和运行实验的说明。我们的工件在OSDI 2022的工件评估过程中获得了“Artifacts Available”、“Artifacts Functional”和“Artifacts Reproduced”徽章。我们的工件的DOI是 https://doi.org/10.5281/zenodo.6586106

工件仓库。所有项目源代码,包括关于如何在REEF和基准测试上构建和运行主要实验的完整说明,都可以在以下git仓库中找到:https://github.com/SJTU-IPADS/reef-artifacts/tree/osdi22-ae