Safe and Practical GPU Computation in TrustZone
背景知识:
youtube GR视频讲解链接:ASPLOS’22 - Session 2A - GPUReplay: A 50-KB GPU Stack for Client ML - YouTube
GPU软件栈:
- 概念:"GPU软件栈"指的是与GPU硬件相关的全部软件组件,通常包括GPU驱动程序、CUDA或OpenCL运行时、GPU加速库(如cuDNN)、深度学习框架(如TensorFlow、PyTorch)、以及与GPU硬件通信的其他软件组件。这个软件栈包括了让GPU执行各种计算任务的必要组件。
- 软件栈具体层次结构及其复杂性:
GPU软件栈存在的问题:
- 漏洞众多:
问题3:慢启动
启动一个GPU工作需要几秒,但是计算只需要几毫秒
上述原因使得GPU栈难以部署,主要是由于GPU的复杂性,复杂性从何而来呢?
GPU是为图形渲染而生的 ,具有下面的特点:
GPU栈不是为ML设计的:“杀鸡用牛刀”
基于上述的背景,论文“GR”引入了GPU replay技术,从而加速移动设备和嵌入式设备的GPU计算。
主要方案:record + replay
- record:开发者将ML负载在完整的GPU栈上运行,只记录必要的CPU/GPU交互,然后发送给目标
- replay:目标设备由于已经有了record阶段记录的所有信息,所以不再需要GPU软件栈,因此GPU relay没有了上述提到的问题,如安全、慢启动、部署困难。
Notes
对于移动设备,在TrustZone**可信执行环境(TEE)**中运行敏感的GPU计算是很有吸引力的。为了最小化部署在 TEE 中的 GPU 软件,重播方法是有前途的:在 TEE 之外的一个完整的 GPU 堆栈上记录 CPU/GPU 交互; 重播 TEE 内部没有 GPU 堆栈的交互。一个关键的难题是,记录过程必须两者兼而有之(1)在安全环境下发生(2)访问要用于重播的确切GPU模型。为此,我们提出了一种新的记录架构,称为GR-T:拥有GPU硬件的移动设备与运行GPU软件的无GPU云服务协作,双方共同使用图形处理器的硬件/软件进行记录。为了克服由此产生的网络延迟,GR-T提供了优化:寄存器访问延迟、推测和仅元同步。这些技术将记录延迟减少了20倍,从几百秒减少到几十秒。基于重放的 GPU 计算与 TEE 之外的本机执行相比,延迟减少了25% 。
1、简介
移动设备上的GPU从前是为了加速图形渲染,现在是为了机器学习推理和联邦学习。
Arm TrustZone 是一个可信执行环境(TEE) ,其中敏感代码与不可信操作系统隔离,确保执行的保密性和完整性。虽然 TrustZone 已经能够隔离 GPU 硬件[15,44] ,但最大的障碍是 GPU 软件栈(简称 GPU stack1) ,它很大[46] ,并且以漏洞能力而闻名[4,5,60]。现有技术转换 GPU 栈[71]或工作负载[7,61,69]以适应 TEE; 然而,它们会导致高工程成本和兼容性损失,这将在第2节中进行分析。
目前现状:能够隔离GPU硬件,但是GPU软件堆栈很大且漏洞很多,无法保障安全问题。
我们最近的工作[57](下面称为 GR)阐明了如何通过 GPU 记录/重放[14,35,41,70]在 TrustZone TEE 中部署精益 GPU 堆栈。介入 CPU/GPU 的边界,GR 会分两个阶段执行一个 GPU 工作负载 W,例如神经网络推理。
(1)记录阶段在完整的 GPU 堆栈上运行 W,并将 CPU/GPU 的交互记录为一系列寄存器访问和内存转储。
(2)重播阶段通过在新输入上重播预先录制的 CPU/GPU 交互运行 W,而不需要 GPU 堆栈。
在这两个阶段中,记录可以在 TEE 之外的安全环境中进行; 在记录完成一次之后,重播可以在 TEE 中重复进行。该播放器可以简单到只有几个 KSLoC(千行代码),几乎没有外部依赖性,并且不包含 GPU 堆栈中常见的漏洞[2,4,5]。
replay:
- record:在有软件栈的云上跑一次训练,把CPU/GPU的交互就下来,类似于编译。
- replay:把交互记录放到GPU上跑。使手机设备脱离软件栈
在第(2)阶段中,当论文提到"只运行软件栈"时,它的意思是在没有GPU硬件支持的情况下运行GPU工作负载。这是通过记录CPU/GPU之间的交互操作,然后在TrustZone TEE中回放这些操作来实现的。在这个阶段,GPU硬件本身不需要运行,因为工作负载是在TEE中复制的。这种方法的好处在于它不依赖于GPU硬件的特定性能或配置,因此可以在不同的硬件平台上实现更好的兼容性和可移植性。
Q:(1)阶段中需要GPU硬件支持吗?答:需要与GPU硬件进行交互,交互会被记录下来
在移动设备上应用 GR 的一个关键缺失是实用性。GR 关键在于记录特定于目标移动设备的 GPU 硬件模型(通常称为 GPU SKU)以重现 GPU 计算。不幸的是,由于移动操作系统不受信任,ML 开发人员无法在目标设备上运行记录器。它们必须在单独的、可信赖的计算机上生成记录,如图1(a)所示。然而,这样做,早期将 GPU 代码绑定到特定的 GPU SKU,偏离了后期绑定的通常做法。通过后期绑定,开发人员以与硬件无关的格式发布 GPU 代码,比如 OpenCL 或 Metal,稍后在目标设备上针对特定的 GPU SKU 进行 JIT 编译。由于早期绑定,GR 要求开发人员预见其工作负载可能在其上运行的 GPU SKU,拥有这样的 SKU,并生成/发布每个 SKU 记录。这给开发人员带来了负担,考虑到现在智能手机上有多达80个 GPU SKU。
这个工作将GR拓展到了移动设备与云结合的场景
- 移动OS不受信----->云环境受信:记录器可以在云上进行记录,执行阶段在移动设备的GPU硬件上执行
研究动机:移动设备上应用GR只能前期绑定,实用性缺失
**关键思想:**提出了一种称为 GR-T 的新方法,允许移动设备(即客户端)利用云进行GPU记录。如图1(b)所示,**云托管的移动 GPU 软件栈没有任何 GPU 硬件。**为了记录,客户端 TEE 请求云运行 GPU 工作负载 W。云在其 GPU 堆栈上“干运行”W,同时将所有由此产生的 CPU/GPU 交互通道到客户机 TEE 中受保护的物理 GPU。云将所有的交互记录为 W 的记录,然后客户端下载这些记录。在未来执行 W 时,客户端 TEE 在受保护的 GPU 上重放录音,而不调用云。使用 GR-T,云记录器可以访问精确的、多样化的 GPU SKU,而无需在云中托管 SKU。
Q:在云上记录就不会产生早期绑定的问题吗?不会降低实用性?为什么?==
答:往后看…
为什么可以信任远程云服务进行记录?(1)记录阶段在设计上不需要工作负载的精确输入(2.3)。因此,客户机 TEE 从不发送敏感数据,如 ML 输入和模型参数。(2) TEE 期望云 GPU 栈是完整的,对此云可以提供很高的保证: 它是由严格的安全措施管理的[32] ; 它可以通过包括英特尔 SGX 和 AMD SEV 在内的技术进行远程验证; 密封在虚拟机中,云中的一个 GPU 栈实例仅通过一个狭窄的接口加密通信服务于一个客户端。这比在客户端不可信的操作系统上运行 GPU 栈要安全得多,后者为无数第三方应用程序提供服务,并面临包括恶意软件和配置不当在内的各种威胁。有关安全性分析,请参见7.1节。
关于安全环境的考虑:云是有第三方保障的,所以不会产生安全问题。
**挑战和设计:GR-T 面临的主要挑战来自于在无线因特网上跨越 GPU 栈(运行在 CPU 上)**和 GPU 硬件。机器学习工作负载导致频繁的 CPU/GPU 交互,包括对 GPU 寄存器的访问、共享内存访问和中断。当 CPU 和 GPU 位于同一个设备上时,每次交互不会超过几微秒。然而,在无线连接中,天真地转发每个交互需要几毫秒或几秒钟。如此长的延迟会排除 GPU 记录由于频繁的软件/硬件超时; 它会使 GR-T 无法使用由于强大的记录延迟。
这里的CPU/GPU交互指的是移动设备上的CPU,和云上的GPU软件栈,移动设备上的GPU硬件
为了克服长时间的延误,我们有两方面的见解。(1) GPU 寄存器访问序列由许多循环段组成,这些循环段是由对 GPU 驱动程序例程的重复调用引起的,如作业提交和 GPU 缓存刷新。通过学习这些访问模式,云服务甚至可以在客户端响应之前推测出大多数注册访问及其结果 (2)不同于云卸载,其中云必须产生正确的计算结果,为了记录云只需要模拟运行 GPU 栈,提取感兴趣的 CPU/GPU 交互。
Q:学习这些访问模式,如何学习?答:后续段落有讲解,其实是基于一些观察实验。
有了这些见解,GR-T自动为以下机制编写GPU驱动程序代码。
(1)寄存器访问延迟。虽然每个寄存器访问都打算在物理GPU上同步执行,但云服务排队并批量提交多个访问到客户端GPU,合并它们的网络往返。由于寄存器按照程序顺序访问驱动程序的执行,因此云服务将暂挂寄存器读取的值表示为符号,并以符号方式执行驱动程序。在寄存器访问完成后,云将符号变量替换为具体的寄存器值。
措施1,对客户端GPU执行延迟的处理:暂挂未知值为符号,以符号形式执行驱动程序。
(2)寄存器访问猜测。为了进一步掩盖提交的网络延迟,云服务预测寄存器读取的结果。在不等待客户端完成提交的情况下,云继续使用预测的寄存器值执行GPU驱动程序,并在客户端返回实际的寄存器值后验证预测。如果预测错误,云和客户端都会回滚到它们最近的有效状态。
措施2,对提交的网络延迟的处理:使用预测值直接进行计算
Q:预测错误的代价是否比延迟本身产生的代价更高?;答:预测很谨慎,预测错误通过后续的一些方法设计,总体代价不高。
(3)仅元同步。尽管是物理分布式的,但是云和客户机必须维护 CPU/GPU 共享内存的同步视图。GR-T 通过接入 GPU 的硬件事件来减少同步频率; GR-T 通过只同步 GPU 的元状态(GPU 着色器、命令列表和作业描述)而忽略程序数据来减少同步跟踪。本质上,GR-T 放弃了计算的正确性,同时忠实地保留了 CPU/GPU 交互的语义。
**结果:**我们在Armv8 soc和Mali Bifrost(一个流行的移动gpu家族)上构建GR-T,并在一系列ML工作负载上对其进行评估。相对于朴素转发,GR-T降低记录延迟超过一个数量级,从几百秒到几十秒。减少客户的能源消耗高达99%。与在TEE之外不安全的本机执行相比,它的重放导致延迟降低了25%
**贡献:**我们提出了一个在TrustZone TEE内的GPU计算的整体解决方案。我们解决了缺失的关键部分——一个安全、实用的录音过程。我们做出以下贡献。一种称为GR-T的新架构,其中云和客户端TEE协作运行GPU软件/硬件进行记录。一套关键的I/O优化,利用特定于gpu的洞察力来克服云和客户端之间的长网络延迟。实用性的具体实现:GPU驱动程序的轻量级仪表;为云虚拟机制作设备树以操作远程gpu;一个TEE模块管理GPU的记录和重播。
2、动机
移动GPUs
这篇论文聚焦于移动和嵌入式SoC上的移动GPU。今天,一个移动GPU已经有了自己的MMU,并且和CPU共享内存。
移动GPU没有自己的内存,它与CPU共享内存。
GPU软件栈和执行工作流: 一个现代GPU栈由ML框架(如TensorFlow),GPU API的用户运行时和一个内核中的GPU驱动程序。图4显示了详情:
当一个APP执行ML工作负载,它调用GPU API,比如openCL。
Step(1)运行时准备GPU工作和输入数据,它发出GPU指令、着色器、数据往映射app地址空间的共享内存。
Step(2)程序设置GPU的页表,存储GPU的虚拟地址和物理地址之间的共享内存映射;配置GPU硬件,提交GPU作业。
Step(3)然后GPU从共享内存中加载作业着色器代码和数据,执行代码,并将计算结果和作业状态写回内存。
Step(4)作业完成后,GPU向CPU发起一个中断。对于吞吐量,GPU堆栈通常支持多个未完成的作业。
描述了工作中GPU和CPU进行交互的场景
CPU/GPU通过三个通道交互:
- 寄存器,用于配置GPU和控制作业。
- 共享内存,CPU存储命令、着色器和数据并检索计算结果。
- 现代GPU有专用的页表,允许它们使用GPU虚拟地址访问共享内存。GPU中断,表示GPU作业状态。GPU驱动程序管理这些交互;因此,它可以介入并记录这些交互。
优先方法
软件栈存在安全隐患,目前的方法是如何解决软件栈的安全隐患的?
在可信区域TEE中面向GPU计算,优先方法是不充分的。
-
方法1:
**将GPU软件栈转移到TEE中:**一种方法是将GPU堆栈拉到TEE(“提升和移位”)[36,51]。最大的问题是笨拙的GPU堆栈:堆栈跨越大型代码库(例如数十MB的二进制代码),其中大部分是专有的。堆栈依赖于POSIX api,这些api在TrustZone TEE中不可用。由于这些原因,移植专有的运行时二进制文件和POSIX仿真层将是一项艰巨的任务,更不用说GPU驱动程序了。
划分GPU堆栈并移植其中的一部分,正如最近的工作所建议的那样[34];[71],也看到了明显的缺点:它们仍然需要很高的工程努力,有时甚至需要定制硬件。移植的GPU代码很可能会给TEE引入漏洞[1,3,4],使TEE膨胀,降低安全性。
问题:GPU stack 较大、POSIX API在TEE中不可用
-
方法2:
**外包:**对TEE来说,另一个方法是调用一个外部的GPU软件栈,另一种方法是TEE调用外部GPU堆栈。一种选择是在同一设备的正常操作系统中调用GPU堆栈。由于操作系统是不可信的,TEE必须防止它学习ML数据/参数并篡改结果。最近的技术包括同态加密[25,69]、ML工作负载转换[29,43]和结果验证[17]。它们支持有限的GPU操作符,并且经常导致显著的效率损失。
在同设备的正常OS中调用GPU 软件栈,导致效率缺失
GR for TrustZone
关于软件栈存在的安全隐患,GR提供方法:记录+重播
不像优先方法,GR提供了一种新的执行范例。(1)在记录阶段,APP开发者在可信的GPU软件栈执行一次ML工作负载。一个在CPU,GPU边界的记录器记录下CPU/GPU的交互——寄存器访问。(2)在重放阶段,一个目标APP调用了一个简单的重放器去在新的输入数据上产生计算结果。
例子:图2举例说明了GR如何用于神经网络(NN)推理。为了记录,开发人员运行一次神经网络推理并产生一系列记录,每个神经网络层一个;每个记录都包含一个神经网络层的多个GPU作业。为了重放,目标ML应用程序按层顺序执行记录。记录的粒度是开发人员在可组合性和效率之间权衡的选择。或者,开发人员可以创建一个所有神经网络层的单片记录(未在图中显示)。
为什么GR有效?它解决了以下三个设计问题。
(1)完整性。发生在CPU/GPU边界,记录透明地捕获了重现原始GPU计算所需的所有信息:GPU命令,着色器,页表和作业输入/输出地址。例如,CPU对GPU地址空间的动态更新记录在GPU页表的快照中;GPU内存布局被记录为内存转储;CPU/GPU同步,例如OpenCL屏障,被记录为GPU中断的寄存器轮询。
(2)决定论。在记录时,GR会采取措施阻止不确定的CPU/GPU交互,例如作业提交时间和并发性。记录器/重放器序列化作业执行并避免并发GPU作业;在录制和回放期间,只有一个应用程序可以访问GPU。这使得CPU/GPU交互具有确定性,确保重放器可以忠实地再现记录的计算。
并发性被避免了,效率也有降低;但是并发了,可能执行结果完全错误
(3)输入的独立性。常见的机器学习工作负载(例如CNN和RNN)通常有GPU作业的静态图;作业之间不存在条件分支,这是先前工作中利用的一个关键特性[72]。**工作负载调用同一组GPU作业,而不考虑其输入数据。**因此,单个记录运行可以运行工作负载中的所有GPU作业并捕获它们。一旦记录下来,相同的GPU作业可以被重复地复制:重播器注入一个新的输入到记录的输入地址,然后可以从记录的输出地址中检索相应的输出。
记录的问题
GR提出的记录+重播的方法,记录环境本身存在一些问题。
**记录环境的要求:**记录环境要求在TrustZone应用GR时,缺少的一个组件是运行GPU软件栈并产生记录的记录环境。首先,记录环境必须对将来用于重播的确切GPU SKU可访问。根据我们的经验,即使是细微的SKU差异也会破坏重放。例子包括GPU硬件资源的变化,例如着色器核心计数,它决定了JIT编译器如何生成和优化GPU着色器;GPU页表格式的变化;共享内存布局的变化,GPU通过它与CPU通信其执行状态。其次,必须是可信的,以保证GPU计算的完整性。损坏的记录可能导致意外的重播结果,甚至破坏整个TEE的完整性。这就排除了客户端设备上的本地记录,因为其正常的操作系统是不可信的,通常由新手用户管理,容易受到恶意软件和标题党攻击。
(1)在安全环境中进行(2)访问要用于重播的确切GPU模型。
**可选的记录环境:**开发人员可以选择以下记录环境,这些环境远离客户端,但仍然缺乏可移植性和实用性。
(1)开发者机器。应用程序开发人员可以维护各种GPU SKU,并为目标客户端生成每个SKU的记录。然而,它们必须应对各种各样、不断变化的移动硬件[63,73]。图3突出了当今移动GPU的多样性:在今天的智能手机上可以看到大约80个SKU;没有SKU占主导地位;经常推出新的SKU。对应用程序开发人员,例如那些开发视频分析或活动识别作为其应用程序的安全扩展来说,预见所有可能的GPU SKU在客户端并拥有它们是不切实际的。更重要的是,由于记录是特定于SKU的,在客户端之间分发它们违反了今天分发ML应用程序的常见做法——**以硬件中立的格式发布GPU程序以实现可移植性。**我们所知道的所有主流ML框架都遵循这种常见做法。
与开发APP的应用服务商机器挂钩。
Q:GR-T本质是去实时做记录,也就是说最终还是需要为每种硬件定制它的对应的记录。那么为什么不提前去做呢?
(2)云中的移动设备工厂。虽然这样的设备群减轻了开发人员的负担,但在云中管理大量不同的移动设备是不切实际的。移动设备不是为托管而设计的,因此不符合数据中心的尺寸、功率和散热要求。设备群不是弹性的:一个设备一次只能服务一个客户端;规划容量和设备类型比较困难。由于每隔几个月就会出现新的移动设备,因此拥有的总成本很高。
3、GR-T
我们提倡一种新的记录方法:当工作负载第一次执行时,在客户端使用GPU时,在云中运行GPU堆栈。
工作流
GR-T工作流如下所示。(1)开发人员像往常一样编写ML应用程序,例如在Tensorflow之上的MNIST推理;他们像往常一样以硬件中立格式发布GPU程序。它们忽略了TEE、GPU SKU和云服务。(2)在首次执行工作负载之前,客户端TEE请求云服务对工作负载进行干运行。当云运行GPU堆栈时,它将GPU硬件访问转发到客户端TEE并接收GPU的后续响应。与此同时,云记录下CPU/GPU的交互。(3)对于机器学习工作负载的实际执行,客户机 TEE 在新输入上重播记录的 CPU/GPU 交互(利用输入独立性) ;它不再调用云。出于安全考虑,即使客户端具有相同的 GPU SKU,云也不会缓存和重用记录。
我们的方法从根本上不同于远程 I/O 或 I/O-Device-as-a-Service [64]。我们的目标既不是在云中执行 GPU 计算[18,21],也不是精确地在云中运行 GPU 栈,例如软件测试[67]。**它是提取 CPU 对 GPU 的刺激和 GPU 的响应。**我们独特的目标允许以后描述新颖的优化。
方法目的:提取CPU对GPU的刺激和GPU的反应【交互】
为什么使用云做记录?
- 安全
- 资源丰富
云可以模拟GPU吗?
有人可能想知道云是否运行基于软件的GPU模拟器[23],而不需要在客户机上使用物理GPU。然而,现代gpu的精确仿真是困难的:它们是多种多样的;它们通常具有未公开的行为、接口和硬件特性。
云会有太多的 GPU 驱动程序吗?
虽然云虚拟机需要为客户端上的所有GPU sku托管驱动程序,但所需的GPU驱动程序总数将很少。这是因为单个GPU驱动程序通常支持同一家族的多个GPU sku [12,13];sku共享很多驱动逻辑,而不同的只是寄存器定义、硬件版本和勘误。例如,Mali Bifrost和Qualcomm Adreno 6xx驱动程序分别支持6和7个gpu[10,47]。正如第6节将展示的,通过指导内核设备树,我们可以将多个GPU驱动程序合并到一个统一的Linux内核映像中,供云虚拟机使用。
SKU具有多样性
为什么GR-T是实用的?
有人可能想知道操作系统和设备供应商是否处在一个很好的位置制作TEE内的GPU软件栈。然而,这样做需要跨多方(例如操作系统、 TEE、 GPU 和 SoC 的供应商)进行深度定制; 特别是,硬件供应商对其 IP 保密。即使他们愿意,他们仍然面临着重新构建复杂的GPU堆栈的挑战。相比之下,GR-T不需要如此深入的跨领域合作。与其他记录环境(4.2)不同,GR-T减轻了从云提供商和ML开发人员那里维护当前和未来GPU sku的负担。
限制?
GR-T需要互联网连接才能工作:当客户端设备不在线时,它无法创建录制。恶劣的网络条件会减慢整个录制过程。要记录工作负载,TEE必须忠实地分配与工作负载实际运行所需相同数量的内存量。由于可用于TrustZone的安全内存通常预先配置得很小[52,53,58],因此GR-T可能需要SoC软件来扩大安全内存以记录高内存ML工作负载。
更广泛的适用性
虽然我们展示了GR-T用于记录,但它的优化可用于远程调试[67]。例如,通过将客户机的GPU注册日志和内存转储与来自云的日志和内存转储进行比较,云可以检测并报告软件故障,供应商可以远程排除故障。由于重播已经在GPU[30]以外的IO设备上使用,我们的技术可以用于为这些IO生成记录,而无需拥有实际的IO硬件。
GR-T的架构
图4显示了该体系结构。云服务管理多个虚拟机镜像,对应不同的GPU堆栈。虚拟机是精简的,包含一个内核和GPU堆栈所需的最少软件。一旦启动,VM就只能专用于服务一个客户机TEE。虚拟机和录音都不能在客户端之间共享。云VM和TEE之间的所有通信都经过身份验证和加密。
GR-T的记录仪包括两个用于云的垫片(Driver- Shim)和客户端TEE (GPUShim)。位于GPU软件栈底部的DriverShim插入了对GPU硬件的访问。它是通过自动检测GPU驱动程序来实现的,注入代码来注册访问器和中断处理程序。GPUShim作为TEE模块实例化,在记录期间隔离GPU并阻止正常世界访问。
在记录运行之后,DriverShim将记录的交互作为记录处理;它签名并将记录发送回客户端。为了重放,客户机TEE加载记录,验证其真实性,并执行所包含的交互。在重放期间,TEE隔离GPU;在重播之前和之后,它重置GPU并清理所有硬件状态。
两个关键的部分:
{ 云端:DriverShim,负责记录 客户端:GPUShim,负责重放 \begin{cases}云端:\text{DriverShim},负责记录\\ 客户端:\text{GPUShim},负责重放\end{cases} {云端:DriverShim,负责记录客户端:GPUShim,负责重放
挑战:长时延延迟
设计一个 GPU 堆栈的前提是 CPU 和 GPU 在片上互连时具有亚微秒延迟。GR-T 通过将 CPU/GPU 以几十毫秒甚至几秒的延迟分布在因特网上,打破了这个前提。影响是双重的。(1) GPU 寄存器访问被转换成许多网络往返。以 MNIST 推理为例,GPU 驱动程序发出2800次寄存器访问,每次访问都需要往返。(2)长的往返时间(RTT)使内存同步变慢。通过设计,CPU 和 GPU 通过共享内存交换广泛的信息:命令、着色器代码和输入/输出数据。由于它们是分布式的,没有共享物理内存,因此维护这样一个共享内存视图的速度非常慢。正如我们将在第五部分展示的那样,经典分布式共享存储处理机(DSM)在演习中错过了一个关键机会
长时间的记录延迟,通常是数百秒(7),使得GR-T无法使用。(1)我们观察到,GPU堆栈不断抛出异常和GPU复位或冻结不时。这是因为长延迟违反了堆栈代码和GPU软件隐含的许多时间假设。(2)由于TEE必须**专门锁定GPU以实现创纪录的运行,它会长时间阻止正常世界的应用程序访问GPU并损害系统交互性。**(3)由于每条记录运行(每个客户端,每个工作负载)需要数百秒的专用云虚拟机,因此GR-T是不合算的。(4) ML工作负载必须等待很长时间才能执行
4、隐藏寄存器访问延迟
为了克服长时间的网络延迟,我们回顾了已知的 I/O 优化,以利用移动 GPU 中的新机会。
寄存器访问推迟
**问题:**通过设计,GPU 驱动程序将 GPU 寄存器访问编织到指令流中,按程序顺序同步执行寄存器访问和 CPU 指令。例如,在清单1(a)中,驱动程序不能发出第二个寄存器访问(第4行) ,直到第一个访问(第3行)和前面的 CPU 指令完成。
由于决定性原因,指令只能同步而不能并发,但是只能同步又导致了大量的网络延迟。
同步寄存器访问导致许多网络往返。由于 GPU 寄存器访问受到读操作的控制(在我们的测量中超过95%) ,这种情况不能简单地以写操作的方式进行缓冲,所以这种情况更加严重。这是在图5(a)中已有的。
基础想法:我们通过使寄存器访问异步来合并往返:如图5(b)所示,DriverShim在驱动程序执行时延迟寄存器访问,直到如果没有从延迟寄存器读取的值,则无法继续执行。然后,DriverShim同步地将所有延迟的寄存器访问批量提交给客户端GPU。提交之后,DriverShim暂停驱动程序的执行,直到客户端GPU返回寄存器访问结果。
为了实现这个机制,DriverShim通过自动检测将延迟钩注入到驱动程序中。驱动程序源代码保持不变。
**正确性的关键机制:**首先,DriverShim保持延迟对客户端和它的GPU透明。为了保证正确性,GPU必须执行相同的寄存器访问顺序,就好像没有延迟一样。寄存器访问必须按照它们的程序顺序,因为(1)GPU是有状态的,(2)这些访问可能有隐藏的依赖关系。例如,从中断寄存器中读取可以清除GPU的中断状态,这是后续写入作业寄存器的先决条件。出于这个原因,DriverShim队列按照程序顺序注册访问。**它为每个内核线程实例化一个队列,**这对后面要讨论的内存模型很重要。
保证GPU执行相同的寄存器访问顺序
其次,DriverShim跟踪数据依赖关系。这是由于
(1)驱动程序代码可能会从未提交的寄存器读取中消耗值;
(2)后一次寄存器写入的值可能取决于先前的寄存器读。
清单1 (a)显示了一些示例:变量qrk_mmu取决于从寄存器MMU_CONFIG读取。第8行对MMU配置的写操作取决于第4行对寄存器的读操作。为此,**对于每个排队读取的寄存器,DriverShim为读取值创建一个符号,并在随后的驱动程序执行中传播该符号。**具体来说,符号S可以在稍后的寄存器写入中编码以排队,例如reg_write(MMU CONFIG, S|0x10)。在下一次提交返回具体的寄存器值之后,DriverShim解析这些符号并替换驱动状态中编码这些符号的符号表达式。
Q:假如有频繁的变量读写操作,这样的维护方式是否会消耗大量的云内存?并且增大传输数据量
第三,DriverShim 尊重控件未提交寄存器读取的依赖关系,如清单1(b)第3行所示。DriverShim 立即解决了这种控制依赖性:它提交所有排队的寄存器访问,包括属于谓词的访问。
Q:那么这种优化是不是对代码本身编写有一定的依赖性?假如上层程序编写者将原本可以推迟的谓词访问放在读写寄存器之后,那么【寄存器访问异步以合并往返+遇到谓词访问时立刻提交所有的排队寄存器访问】效果将不再明显(表现为推迟了寄存器访问,马上又进行下一次寄存器访问),是不是可以结合编译器优化的方式对程序予以修改?
什么时候提交? 当驱动程序触发以下事件时,DriverShim 提交寄存器访问
-
解析控制流依赖:当驱动程序执行即将接受依赖于未提交寄存器读的条件分支时,就会发生这种情况。
-
调用内核APIs:特别是调度和锁定。有三个原因。
- (1)通过这样做,DriverShim 安全地将代码插装和依赖跟踪的范围限制在 GPU 驱动程序本身; 因此它避免了对整个内核这样做。
- (2) DriverShim 确保所有寄存器读取在可能外部化寄存器值的内核 API 之前完成,例如寄存器值的
printk ()
。 - (3)在任何锁定操作(锁定/解锁)之前提交寄存器访问确保内存一致性,这将在下面讨论。
并发线程的内存一致性
GPU 驱动程序的设计是多线程的。由于 DriverShim 延迟了对每个线程队列的寄存器访问,如果一个驱动程序线程为变量 X 分配了一个符号值,那么对 X 的实际更新将不会发生,直到该线程提交相应的寄存器读取。如果此时另一个线程试图读取 X 怎么办?它会读取 X 的过期值吗?
DriverShim 实现了一个已知的重新释放一致性内存模型[27] ,以确保没有其他并发线程可以读取 X。内存模型由两个设计保证。
(1)前提:锁的保障。鉴于 Linux 内核和驱动程序已经被彻底检查了数据竞争[49] ,一个线程总是用必要的锁更新共享变量(例如 X) ,这阻止了对变量的并发访问。
(2) 措施:DriverShim 总是在驱动程序调用解锁 API 之前提交寄存器访问,即线程在释放任何锁之前提交寄存器访问。因此,在允许任何其他线程访问这些变量之前,线程必须使用具体的值更新共享变量。
问题:推迟寄存器访问可能造成并发的线程读取旧值。
解决手段:利用已有的锁机制,解锁时强制提交寄存器访问。
-
驱动器有明显延迟。例如,调用内核的延迟函数族[48]。驱动程序经常使用延迟作为屏障,假设在程序顺序中
delay()
之前的寄存器访问将在delay()
之后生效。例如,驱动程序编写一个 GPU 寄存器来启动缓存冲刷,然后调用delay ()
,在此之后,驱动程序期望缓存刷新已经完成,并且一致的 GPU 数据已经驻留在共享内存中。为了尊重这样的设计假设,DriverShim 在显式延迟之前提交寄存器访问。
提交寄存器访问的时刻:
- 控制依赖:条件分支
- 内核API调用:比如对共享变量的解锁
- 驱动器有明显延迟:可能是驱动器在利用延迟进行cache的刷新,所以也要提交
最优化
为了进一步降低开销,我们缩小了寄存器访问延迟的范围。我们利用一个观察: GPU 寄存器访问在驱动程序代码中表现出高的局部性:数十个“热”驱动程序函数发出超过90% 的寄存器访问。这些热函数类似于 HPC 应用程序中的计算内核。
为此,我们通过分析函数获得热函数列表。我们运行 GPU 堆栈,跟踪寄存器访问,并通过驱动程序函数对它们进行垃圾处理。在记录时,DriverShim 只延迟这些函数中的寄存器访问。当驱动程序的控制离开这些热函数时,DriverShim 提交排队的寄存器访问。
注意: (1)热函数的选择是为了优化,并不影响驱动程序的正确性,因为热函数之外的寄存器访问是同步执行的; (2)每个 GPU 驱动程序只进行一次探测,因此工作量很小。
猜测
基础猜想:即使使用延迟的寄存器访问,每个提交仍然是采用一个 RTT 进行同步的(图5(b))。Driver-Shim 进一步异步提交来隐藏它们的 RTT。这个想法如图5©所示:而不是等待提交 C 完成,DriverShim 预测 C 中包含的所有寄存器读取的值,并继续使用预测值执行驱动程序; 稍后,当 C 完成实际读取值时,DriverShim 验证预测值: 如果所有预测都是正确的,它继续驱动程序执行;否则,他初始化一个恢复进程。错误预测会导致性能损失,但不会违反正确性。
不等待提交,直接使用预测值+回滚的方式
Q:错误预测导致的性能损失有多少?预测本身需要多少代价?会产生多少错误预测?
答:回滚的损失由避免级联回滚保障;预测代价可能是对历史记录的维护,会产生一定的开销;预测较为谨慎,且有回滚机制,错误预测应该是风险可控的。
**为什么寄存器值是可预测的?**我们的观察是,驱动程序发出寄存器访问的重复段,GPU 大多数时候以相同的值响应。这样的分段在工作负载中(例如 MNIST)重复出现以及跨工作负载(例如 MNIST 和 AlexNet 推断)
有重复段,会以相同的值回复
是什么导致了重复的部分?
(1)GPU的日常维护。例如,在每个 GPU 作业之前和之后,驱动程序使用 GPU 的 TLB/cache。寄存器访问和寄存器值的序列(例如冲刷操作的 最终状态)重复它们自己。
(2)重复的 GPU 状态转换。例如,每当一个空闲的 GPU 唤醒时,驱动程序就会执行 GPU 的电源状态机,为此驱动程序发出一个 x 序列的寄存器写入(启动状态更改)和读取(确保状态更改)。
(3)重复的硬件发现。例如,在初始化过程中,驱动程序通过读取数十个寄存器来探测 GPU 硬件功能。当硬件不变时,寄存器值保持不变。
举的例子通常是特殊的情形,非一般程序中的寄存器访问,不是很具有通用性
Q:对于举出的例子,可以认为对于更加一般的寄存器访问,在大多数情况都是不能预测的,所以大多数情况应该都会等待RTT返回结果吧?该预测方法对于降低RTT不高。
何时推测?并非所有的寄存器访问都属于循环段。为了尽量减少错误预测,DriverShim的行为比较保守,只有在他的提交历史显示出很高的可信度时才进行预测。
- 具体的推测算法:
当 DriverShim 准备进行提交 C 时,它将在同一驱动程序源位置查找提交历史记录。它考虑了包含与 C 相同的寄存器访问序列的最新k次历史提交。如果所有的 k 历史提交都读取了相同的寄存器值序列,DriverShim 将使用这些值进行预测;否则,Driver-Shim 将避免 C 的推测,而是同步执行它。k是一个允许预测的可配置参数控制置信区间。我们在实验中设定 k = 3。
使用过去的k次提交的值进行当前值的预测
Q:这个k=3是通过何种方式确定的?是不是可以通过一些启发式算法?有没有做具体的实验?或者后验时,若预测错误,对k值进行反馈,比如增加k值,提高对历史情况的信任度
这里的历史提交记录是需要额外维护的吗?那么这样维护会不会产生较大内存开销?
现在内存比较便宜,可能内存开销不是一个很重要的问题
驱动程序如何使用预测值执行?
- 可能出现的情况:根据预测的寄存器值,GPU 驱动程序可能会改变其状态并获取代码分支;DriverShim 可能会在不等待未完成的提交完成的情况下进行新的提交。
- 采取的措施:为了确保正确性,DriverShim 会停止驱动程序的执行,直到所有未完成的提交都完成,并且当驱动程序将要外部化任何内核状态时(例如对变量调用 printk ()) ,预测被验证。这个条件很简单,因为它不区分外化状态是否依赖于预测的寄存器值。因此,检查条件是微不足道的: DriverShim 只是拦截了十几个可能外部化内核状态的内核 API。DriverShim 避免在整个内核中对数据和控制依赖进行单粒度跟踪。
“externalized kernel state” 意味着将 GPU 内核状态(kernel state)以某种方式提供给外部,通常是通过使用
printk()
或其他内核API进行记录或日志输出。——正式要用某个值了
优化:只检查上述条件有一个缺点:在预测错误的情况下,驱动程序和 GPU 都必须回滚到有效状态,因为它们都可能基于预测错误的寄存器值执行。清单1(b)显示了一个例子:如果在第二次提交后发现 JOB_IRQ_STATUS
(第2行)的读取被错误预测(第10行) ,驱动程序已经包含一个不正确的状态(在 dev 中) ,并且 GPU 已经执行了不正确的寄存器访问(例如写入 JOB_IRQ_CLEAR
)。
为此,DriverShim 可以在预测错误的情况下减轻客户端 GPU 的回滚。
方法:避免级联回归
这是通过避免向客户泄露投机状态发挥作用的。具体来说,Driver-Shim 在提交注册器访问之前会额外地停止驱动程序,这些注册器访问本身是推测性的,即对预测值具有依赖性。例如,在清单1(b)中,如果第一次提交尚未完成,则第二次提交必须停止,因为第二次提交包含随意依赖于第一次提交结果的寄存器访问(JOB_IRQ_CLEAR
和 TILER/SHADER_PREENT
)。为了跟踪推测的寄存器访问,DriverShim 污染了预测的寄存器值,并在驱动程序执行中跟踪它们的数据/控制依赖关系。在上面的示例中,当驱动程序根据推测值(第3行)获取条件分支时,DriverShim 将该分支上所有更新的变量和状态污染为推测值,例如 dev-> tiler。为了完整起见,污点跟踪应用于驱动程序调用的任何内核代码。
如何从错误预测中恢复?
当 DriverShim 发送的实际寄存器值与预设值不同时,GPU 堆栈和/或 GPU 应该恢复到有效状态。我们利用 GPU 重播技术[57] ,使双方能够独立地重置和快进。为了启动恢复,DriverShim 向客户机发送交互日志中错误预测的寄存器访问的位置。然后双方重新启动并将日志重放到该位置。在这个过程中,GPUShim 将记录的刺激(如寄存器写入)反馈给物理 GPU;DriverShim 将记录的 GPU 响应(如寄存器读取和中断)反馈给 GPU 堆栈。因为双方都不需要网络通信,所以恢复只需要几秒钟的时间,我们将对此在7.3节进行评估。
恢复需要很短的时间,后验的值可以重新用来进行正确的计算
卸载轮询循环
GPU 驱动程序经常调用轮询循环,例如,忙于等待寄存器值的更改,如清单2所示。轮询循环占寄存器访问的很大一部分; 它们是控制依赖关系的主要来源。
**问题:**轮询循环的简单执行会导致多次往返,使得上述技术无效。(1)延迟寄存器访问没有多大好处,因为每次循环迭代都会产生控制依赖项并请求同步提交。(2)对轮询循环的推测是困难的:通过上面的设计,DriverShim必须预测满足终止条件的迭代计数,这通常取决于GPU定时(例如GPU作业的延迟),并且通常是不确定的。
类似于上述的if条件判断语句的问题,那么循环的问题被解决了,但还是存在优化依赖于编程者习惯的可能
**观察结果:**幸运的是,大部分的轮询循环是简单的,满足下面的条件:
- 循环中的寄存器访问是等幂的: 循环体的重新执行不会影响 GPU 状态
- 迭代计数只有局部影响: 计数是一个局部变量,不会转义包含循环的函数。计数是用一些简单的谓词来计算的,例如
count < MAX
。 - 循环中引用的内核变量的地址是在循环之前确定的,也就是说,循环本身并不动态地计算这些地址
- 循环体不会调用具有外部影响的内核 API,例如锁定和
printk ()
。
DriverShim 使用静态分析去找到 GPU 驱动程序中的所有简单轮询循环。复杂的轮询循环模糊了上面的定义,这种情况很少见; DriverShim 只是执行它们而没有进行优化。
**解决方案:**DriverShim执行简单的轮询循环如下:
(1)卸载。DriverShim 向客户端 GPU 提交了一个循环,只产生了一个 RTT。为此,DriverShim卸载循环代码的副本以及循环中要引用的所有变量。GPUShim运行循环并返回更新后的变量。卸载尊重4.1节中描述的释放内存一致性,因为对循环内共享变量的访问必须用锁保护,并且循环本身不会解锁。
方案1:涉及到轮询时,只进行一次寄存器请求访问,将循环体放到客户端上执行
(2)投机。DriverShim在卸载循环时进一步掩盖了RTT。与预测精确的迭代计数(例如清单2中max的最终值)不同,DriverShim提取并预测迭代计数上的谓词,例如(max?=0)
,这更容易预测。当客户端返回实际的迭代计数时,DriverShim计算谓词以验证预测。
5、内存同步
**问题:移动 CPU 和 GPU 旨在共享物理内存。由于驱动程序(云)**和 **GPU (客户机)**在它们自己的本地内存上运行,我们需要在它们之间同步一个共享内存视图,如图6所示。内存同步一直是分布式执行中的核心问题[8,18,27,67]。一种经过验证的方法是放松内存一致性: 只有当其他节点即将看到更新时,一个节点才会将其本地内存更新推送到其他节点。因此,先前的系统根据程序行为选择同步点,例如在函数调用边界同步线程本地内存[18]或作为锁/解锁操作的一部分同步数据竞争程序的共享内存[27]。
之前的CPU/GPU共享内存同步方法:当其他的节点看到更新时,一个节点才会将本地内存更新推送到其他的节点。
与这些先前的系统不同,CPU 和 GPU 之间的内存共享协议从来没有显式地确定过。例如,他们从不使用锁。根据我们的观察,我们推测 CPU 和 GPU 写入不相交的内存区域,并通过一些寄存器访问和一些驱动程序注入的延迟对它们的内存访问进行排序。然而,基于这种模糊的假设构建 GR-T 将是脆弱的。
方法:我们的想法是约束 GPU 驱动程序的行为,这样我们就可以对内存同步做出保守的假设。为此,我们将驱动程序的作业队列长度设置为1,这将有效地序列化驱动程序的作业准备和 GPU 的作业执行。这样的约束已经应用在以前的工作中,**显示了较小的开销[57]。**有了这个约束,驱动程序只有在 GPU 空闲时才向共享内存发送 GPU 作业; 只有在驱动程序空闲时,GPU 才从内存执行作业。因此,驱动程序和客户机 GPU 永远不会同时访问共享内存。
Q:将驱动程序的作业队列长度配置为1,以实现串行化的作业提交,是不是可能会导致一定的延迟,甚至限制性能?因为驱动程序必须等待GPU完成当前的作业执行后,才能提交下一个作业,由此产生的等待时间和延迟在系统中是能够容忍的吗?而且假如面临的是多GPU系统,因为要协调多个GPU之间的同步,这样的安排是不是会更加复杂?
何时进行同步? 当 GPU 即将变得忙碌或空闲时,云和客户端将进行同步:
- 云 ⇒ \Rightarrow ⇒客户端:就在开始一个新的 GPU 作业的寄存器写入之前,DriverShim 转储分配给 GPU 的本地内存并将其发送给客户机。内存转储是一致的:此时,GPU 驱动程序已经发出并运行了新作业所需的所有内存状态,并更新了用于映射内存的 GPU 页表。
- 客户端 ⇒ \Rightarrow ⇒云:就在客户端 GPU 发出中断信号完成任务之后,GPUShim 转发中断并将其内存转储上传到云。内存转储也是一致的: 此时 GPU 必须将作业状态和作业数据从缓存写回本地内存
我们进一步实施连续验证作为一个安全网。在 DriverShim 将其内存转储发送到客户机之后,它将从 CPU 取消映射转储的内存区域,并禁用 DMA 与内存之间的通信。因此,对内存区域的任何假访问都将作为页面错误被捕获到 DriverShim,并报告为错误。同样,当 GPU 空闲时,GPUShim 从 GPU 的页表中取消映射共享内存; 任何来自 GPU 的虚假访问都将被捕获。
**同步什么?**如图6所示,我们使内存传输量最小化,具有以下见解: 对于记录,只同步内存中的 GPU 元状态(包括 GPU 命令、着色器代码和页表)是足够的。不需要同步程序数据,例如输入/输出和中间 GPU 缓冲区。幸运的是,程序数据占据了 GPU 内存的大部分。