navycloud 青云


  • 首页

  • 关于

  • 归档

AMD GPU虚拟化的按需调度策略

发表于 2018-07-27

青云原创, GPU 虚拟化

转载请注明出处:https://navycloud.github.io/2018/07/27/on-demand-scheduling/

本文主要内容是给大家分享一下笔者最近在工作中关于GPU虚拟化调度策略的一点小小的研究成果,因此并不会详细地讲解AMD GPU虚拟化技术原理与实现,仅从概念上带过,目的是让观众关注调度策略本身。关于AMD GPU虚拟化基本原理与实现我会抽空另写一篇。

note: 本文面向对GPU硬件和驱动有一定基础的同学,当然没有基础也可以嗑瓜子看看

AMD GPU虚拟化方案简介

首先简单介绍一下AMD 的GPU 虚拟化方案。

AMD给客户的虚拟化方案整体上是一套基于SR-IOV的硬件虚拟化方案(关于SR-IOV读者可以自己google之),所谓基于硬件的虚拟化就是尽量用硬件和firmware去实现GPU的基本功能,且尽量维持物理机GPU驱动软件的复用程度。Intel的方案是基于软件的GPU虚拟化,他们的方案中guest KMD(guest kernel mode driver) 和bare-metal(物理机系统称为bare-metal)相比有较大的改动,主要是command submit和memory manage部分,比如intel的command submit在guest KMD中实际上并不会把command 提交到hardware上,而是通过hyper call的方式提交给HMD (hypervisor module driver),当然hyper-call只是一种说法,实际的做法可以通过修改QEMU来实现而不一定必须是hyper-call,例如修改QEMU代码,创建一种新的虚拟GPU 设备,并且为这个设备的MMIO(寄存器)做trap。当虚拟机 access这些MMIO 时QEMU就会拦截并调用vendor实现的callback函数,从而实现hyper-call的作用,例如guest需要吧一段虚拟机内存映射到GPU 地址空间时就会去设置一些MMIO寄存器, 而这些MMIO寄存器都是QEMU 虚拟出来的并且可以截获,从而让HMD就有机会把这段guest physical address 转成 host physical address 后再映射到GPU 地址空间,把 GPA 专为 HPA的原因是:GPU只能访问真正的物理内存地址,虚拟机中的guest physical address对于GPU而言是无意义的。

注:以上对intel GPU虚拟化方案的技术描述完全是我个人不责任地主观臆断,且没有任何事实依据,本人暂时也没时间去研究 Intel的虚拟化开源代码,intel的同仁如果觉得我胡说八道请轻微diss一下即可。

反观AMD的方案,由于利用SR-IOV 这个标准,由此在host端可以看到多个(目前最大16个)virtual function device, 也就是虚拟出来的PCIe 显卡(简称VF),然后把这些VF pass through 给QEMU虚拟机,因此从软件上讲QEMU完全不知道这些VF 是虚拟出来的。那么VF 能不能完全像物理GPU 一样进行绘图渲染呢?

你猜到了,并不能!AMD的VF 其实并不是完全的硬件虚拟化设备,它其实有只有若干block是硬件级虚拟化的(硬件级虚拟化就是说每一个VF 都有各自独立的ASIC电路为这些block服务),以下这些block被真正硬件虚拟化了:

  • IH (硬件中断服务, 每个VF 都自己独立的MSI/MSIX 硬件服务)
  • GMC (GPU memory controller),每一个VF 都有自己独立的MC 模块,从而每一个VF 都有自己的GPU地址空间,完全独立运作
  • IOMMU (这个是主板芯片组上的硬件,不是AMD GPU中,我把它列在这里是因为IOMMU 的作用本质上是GMC模块在虚拟化方案中的延申和继承,后面详细描述)

好了,接下来就是非硬件虚拟化的block了,说来也无奈,这个非硬件虚拟化的block才是GPU中最复杂,最庞大的模块:Graphic Pipeline,没错!就是这个GFX Pipeline 负责渲染你吃鸡时看到的所有的花草树木,建筑,敌人。。。AMD的人简称它为pipeline或GFX(计算机3D图形学就有pipeline这个软件概念,对应到GFX engine上就有硬件pipeline了,硬件pipeline实现了软件pipeline定义的功能)。

AMD的GPU虚拟化方法和CPU的进程切换是类似的,即分时复用:AMD的HMD(称为GIM)会给每一个VF 一定的时间片(例如6毫秒),时间片内GFX属于它,到点后就把GFX调度到另一个VF,如此每个VF 并不知道它此时是否占有GFX,也不需要知道。每一个VF只需要提交command到GFX ring上就可以了(GFX ring的作用就是让OpenGL/D3D 等User Mode Driver 提交cmd的一段环形buffer,CPU写,GPU读取并执行),当时间片切到它时cmd会被处理,当时间片用完后没有完成的cmd会被抢占(称之为MCBP,Middle of Command Buffer Preemption, 我会抽空写一篇文章简单描述AMD的MCBP实现),抢占后GFX 会被调度到另一个VF,当下一次之前被抢占的VF 得到GFX 时,它之前被抢占的那个cmd会继续执行 (resume execute)。

AMD的GPU和Intel的一样,都需要访问真正的物理内存地址,因此必须要把GPA 转为 HPA。由于QEMU知道有VF pass through 给了虚拟机,因此在虚拟机开机前QEMU 就为每一个VF建立了一张地址转换表格,该表格实现GPA 2 HPA,因此一旦有command提交到VF的GFX ring (GFX ring所占用的地址同样被IOMMU 转换后再让GPU 读取)后 GFX 正常执行,当GFX/GPU需要读取内存时,GPU会把地址送给GMC,GMC会把地址送出到PCIE总线上的IOMMU,IOMMU会按表格先把GMC给的GPA 转为HPA,并读取内容,最后把内容返回给GMC(如果是GPU 写入内存也是类似的套路)。因此整个过程除了两个硬件模块(GMC,IOMMU)没有其他软件或者硬件模块知道这件事儿,GPA 2 HPA对其他模块来说就是透明的。

当然GPU也不是访问任何地址都需要IOMMU, 当GPU 访问自己的显存(local Video Memory)时完全在GMC内部decode地址,所以性能非常好,不走IOMMU 的translate。

综上所述AMD的虚拟化方案不需要修改大量的KMD代码,只要改大约10%的代码就能把普通的KMD 运用到虚拟机里面,原理简述到此为止,现在步入正题。

AMD GPU 虚拟化现有调度方案及其缺点

在本文发表前AMD使用的GPU 调度方案称为round-robin模式,顾名思义就是一个一个轮流来,每个VF 一段时间片,未来一段时间内AMD会给某些客户试用笔者开发的新模式,我称之为on-demand模式,当然名字可能会被高层改得更优雅诗意一点,毕竟我只是个搬砖的粗人。

round-robin 模式简单说就是给每一个VF 一段固定的时间片,用完就对该VF做抢占(抢占就是让GPU停下来,类似于CPU的进程切换,都需要一些时间开销),抢占后再调度到下一个VF,我们这里举例说明这个方法的缺点,请看下图(下图假设每个VF 分配4毫秒时间片):

请仔细看这张图,它清晰准确地反映了round-robin模式最大的缺点:

该图给定两个VF分别对应两个虚拟机,每个虚拟机的CPU 都在同一时刻对各自的VF提交了一个cmd,假设VF0恰巧先得到了时间片(如图),由于该cmd只需要2毫秒就能完成,于是VF0没有任何延迟地就完成了工作,共花3毫秒完成了这一桢(假设GPU花费2毫秒,CPU收到中断通知,检查结果,汇报给UMD花费1毫秒)。由此可以算出对VF0而言这一桢的fps是1000/3 即 333桢每秒。

VF1也在跑同样的游戏,但这一帧却得到了差很多的结果:VF1的CPU提交cmd后,由于GPU不在VF1上,因此过了4毫秒它才得到GPU资源,然后同样花2毫秒完成这一桢,CPU再花1毫秒善后,最终这一桢共花了7毫秒,可以算得fps为 1000/7 即 140桢每秒, 只有VF0的一半都不到。

以2VF 为例,下面举例两种典型的悲剧case:

  1. 此时VF0占有GPU资源的这5ms内有cmd扔给VF0处理,因此VF0短时间内FPS达到最高,然后VF1忍受这低延迟,低fps。如果我们粗糙地用平均fps去统计目前的性能,也许我们会一位VF0+VF1的平均fps并不低嘛,但仔细一看,VF0的fps是200桢,而VF1只有50,因此VF1的玩家是不满意的,而VF0的玩家也没有得到非常大的满足,毕竟超过60的FPS已经意义不大了。
  2. 此时VF0占有GPU 资源的这5ms内并没有cmd扔给VF0处理,例如此时VM0的CPU在做一些非图形相关的计算等,或者游戏开了VSYNC所以还没有cmd扔给VF0处理,那么这个就更杯具了,因为这5ms内VF0和VF1都没有得到GPU 服务。

另外,由于AMD显卡的按时调度是基于MCBP抢占的,而MCBP抢占往往不是立刻成功的,因为MCBP只能发生在gfx 的pipeline完成后,因此5ms时间片内如果VF0一直在忙的话,MCBP命令下达之后往往还需要过几ms甚至几白ms才能调度到另一个VF上,这也是另一个latency问题的主要原因。

BTW:AMD把GPU调度工作称为“world switch”,每一个虚拟机是一个world,把GPU从VF0调度到VF1 就是一次world switch作业。

为什么需要这套”on-demand“调度策略

显然round-robin的方法只有在一种情况下不会白白浪费GPU资源,那就是任何一个VF 都在永远不停地提交cmd给GPU,但这显然是少数情况,对于多数云游戏客户而言,虚拟机只会跑一个GPU 负载不太大的游戏,因此这种情况下GPU 大部分时间都是闲置的,而且还会出现每个VF都时不时掉帧,卡顿的现象,就像上图中的VF1,这个现象我们称之为”latency“。所以需要一套新的调度策略尽量避免round-robin策略的浪费GPU资源的问题。

为了解决这个问题,新的按需调度策略需要从如何不浪费GPU资源的角度入手:当VF得到GPU后如果占着茅坑不拉屎没有cmd执行,需要立即主动让出GPU 资源(yield 机制)。

新的”on-demand“调度策略

按需调度策略的要点是三点:

  1. 【yield 机制】虚拟机需要在确认自己不用GPU资源时竟快通知hypervisor,并且hypervisor需要竟快做world switch
  2. 【电梯算法】每次做world switch前,hypervisor需要先check一下这个VF是不是有pending cmd(必须实现非常快速check否则就是浪费GPU时间),如果没有的话压根就不用调度给他GPU 资源,直接下一个即可。
  3. 【锁死桢速度】虚拟机运行的游戏最好给他定一个vsync,例如60hz或30hz,如此一来当一贞画完时,该虚拟机不会立刻submit下一帧的cmd,而是会有一定时间的等待,这就可以激发要点1。

第一点和第二点是最核心的,第三点根据客户实际情况可以调整。下面说说怎么做到第一和第二点。第一点我给它起了一个名字来描述这个feature – ”VF yield“,顾名思义VF 可以yield自己从而提前让出GPU资源

1. YIELD 机制

该feature需要两个软件模块同时配合,1)guest的KMD; 2)hypervisor端的HMD – GIM (GIM是 在hypervisor端的一个HMD模块,主要职责就是执行world switch)。 guest KMD 需要有一个方法可以立即判断出自己当前时刻是否不需要GPU资源,如果不需要就立刻通知GIM,GIM收到通知后立刻做world switch。

guest KMD 如何判断此时此刻是否需要GPU资源? 这个方法是一个trick,但是目前我只在Linux系统上实现了,windows guest需要用其它方法变通实现,我暂时只关注Linux,下面讲在Linux guest上的实现:

在gpu scheduler中 (gpu scheduler 是指guest kmd中的模块,并不是GIM中的做world switch的调度器,不熟悉AMD软件的童鞋比较容易困惑,这个模块的功能是公平地把每个进程的cmd公平的交错提交给GFX ring),每一个cmd提交到ring buffer后,都会在gpu scheduler维护的一个双向链表中插入一个job来代表该cmd,当一个jcmd完成后,中断会通知CPU,CPU 会把相应的job从该双向链表中删除,整个过程都是有同步锁保护,这个链表可以完美地反映对这个VF 而言哪些job/cmd正在运行,哪些刚刚完成,哪些还没开始。

note: 该双向链表叫 “ring_mirror_list”

下面讲这个trick,当一个job提交时,如果判断mirror list是空的,就说明当前ring突然从idle状态切换到了busy状态,我把这种case称作“idle2busy”,如果job提交时mirror list 不是空的,说明这个job提交的时刻该VF 上已经有cmd正在让GPU 处理中(是逻辑上正在让GPU 处理中,因为GPU 此时可能调度在另一个VF 上),因此这个job的提交就不会触发 “idle2busy”。ok理解了这点再继续理解与之相反的一个概念— “busy2idle”: 当一个job完成时中断会通知CPU,于是CPU会把这个job从mirror list中remove掉,然后再去check mirror list,看是否mirror list已经empty了,如果empty那就说明这个刚刚finish的job是这个VF 最后一个需要处理的job,且这一时刻没有其他cmd 悬挂在GFX ring 上,因此是一个“busy2idle” 的状态切换。

有了以上tricks,guest KMD 可以对每一条ring (AMD的GPU比较复杂,除了GFX ring外还有很多其他ring,例如DMA ring, compute ring, multimedia ring)瞬间检查出是否不再需要该ring所对应的GPU 资源(有cmd完成时可以检测出是否为idle2busy),或者该 ring是否有一个job初次提交从而可以知会GIM自己需要被调度,不要被电梯算法直接skip掉(有cmd提交到ring时可以检测出是否有busy2idle)。如果不在guest KMD的gpu scheduler里面做手脚,而是用measuring的方法去测量GPU是否idle,那会非常低效率,因为测量这个过程本身就需要时间去取采样点,而测量所消耗的时间就是对GPU资源的白白的浪费。

note: ring buffer的概念非常普遍,Intel,AMD,Nvidia 都有,这里就不赘述了,不懂的同学自己baidu/google吧,简单说就是一个环形buffer,GPU 读取cmd并执行,CPU 写入cmd。

由于AMD的GIM是一次world switch同时作用于 GFX/COMPUTE/DMA 这些GPU资源的,所以我们不能仅仅针对GFX ring 采用这个trick去监测,要对所有的ring都check,具体实现是:

  1. 当任意一条ring有cmd提交时,如果判断这个job是触发了”idle2busy”, 则:set_bit(ring->index,&rings_status),用以表示该ring是busy状态。(每条ring都有一个索引 index)
  2. 当任意一条ring有cmd完成时,如果判断ring buffer已经空了,就认为该job触发了“busy2idle”, 则 :clear_bit(ring->index, &rings_status),用以表示该ring是idle状态了
  3. 当任意一条ring触发了”busy2idle“事件,立马check 是否rings_status为0,若为0则说明此时此刻所有的ring都idle,于是立刻通知GIM 这个VF需要”yield“(AMD用中断方式让guest发消息给GIM),GIM收到YIELD通知后立刻做 world switch,从而赚取了这个VF 本来白白浪费了的时间片。当然如果不幸在world switch后该VF 又立刻有cmd提交那也是天命如此,不必钻牛角尖,这个世界上没有完美的调度方案。

2. 电梯算法

电梯算法这个名字是我老板起的,他在两年前首次创新性地提出了这个想法并实现了它,不过当时他是在GIM中用读取寄存器的方法来判断的(可以读取某些寄存器判断给定的VF 是否有pending job,没有的话跳过它下一个)。由于一些硬件的policy原因,电梯算法不能在MI25上用了,有部分VF寄存器不再允许访问,因此我实现了一个纯软件的实现:同样是利用”idle2busy”事件,方法是:

  1. 若任意一条ring触发“idle2busy”, 则:set_bit(ring->index,&rings_status),用以表示该ring是busy状态。(此条规则上面YIELD机制提过) ,并且检查是否 (ring_status & ring_status-1) == 0, 如果为 0 就发中断通知GIM标记此VF为BUSY,如此该VF 下次被GIM轮询到时就不会被skip掉。(ring_status & ring_status-1) == 0 说明ring_status只有一个bit是1,也就是意味着此次触发“idle2busy”事件的ring是整个GPU资源上第一个触发“idle2busy”的,因此这个时机通知GIM是合理的,我们并不需要每一条ring在触发”idle2busy”后都通知GIM,因为中断次数经量要少。
  2. 当任意一条ring触发了”busy2idle“事件,立马check 是否rings_status为0,若为0则说明此时此刻所有的ring都idle,于是中断通知GIM需要对该VF 标记为IDLE,如此下次轮询到该VF时可以skip掉它。
  3. 在GIM端:每次GIM做world switch时可以先检查该VF是否为BUSY,若为BUSY就正常调度它,否则就跳过。

3. 效果如何?看图说话

下面请看使用了“on-demand”策略后的图:

下图为原始的round-robin策略,贴出来方便对比:

为了实现整个思路,GIM里面的world switch调度实现改写了很多,因为需要做到实时性(即是一旦VF 要求yield,GIM要立刻做world switch,否则就会浪费GPU宝贵时间片),而原始的GIM在实时方面做得不够好,笔者的实现基本做到了50us以内响应guest发起的yield信号并开始执行world switch,原本的GIM需要300 ~ 500 us。

目前在MI25(vega10)上实现了这个新的调度策略,过段时间有空的话我会把该策略移植到S7150(tonga)上,毕竟S7150有不少国内客户正在使用,确认合法后我会把s7150的方案源代码上传到我自己的github(GIM + guest KMD)。

实现这套调度算法,我并不是希望单纯得提高所有VF的平均VPS (但这确实达到了这个个次要目的),我的目的是减少latency的频率,从而提高每一个VF的用户体验。现在总结一下为什么新的算法可以提高每一个VF的相应速度:

  1. 由于一旦VF发现自己不需要GPU,在时间片内它仍旧可以立即yield从而让出GPU资源,因此GPU的利用率得到了提高(结合开启vsync后,每个VF 都会有更多的机会检测出自己可以yield出GPU资源,因此效果更好),同时其他VF的cmd被更快得执行了(相对与round-robin)
  2. 由于GIM在调度GPU给一个VF前会用电梯算法检查它是否有work load,没有的话直接跳过该VF,因此GPU的利用率得到了提高,同时其他VF的cmd被更快得执行了(相对与round-robin)
  3. 由于MCBP本身是往往会引起一定时间的开销,而主动让出GPU资源就很好的规避了MCBP的发生(主动让出是因为检测出暂时不需要GPU资源,因此一定不是cmd执行了一半没有结束的状态,因此此时GIM对该VF下达MCBP命令后可以瞬间完成),避免了MCBP的发生就是节省了GPU的时间,因此同时其他VF的cmd被更快得执行了(相对与round-robin)

注:AMD官方的S7150的guest代码已经upstream了,下载最新的kernel就有,S7150的GIM代码没有upstream,但是open source了,在GIT上有,我不记得具体地址了。。。

4. 为什么windows guest需要用其他方法变通实现YIELD

对于windows guest,由于它的gpu scheduler是微软自己实现的,因此逻辑都在windows 内核中,于是windows KMD 没有办法抓到ring的status,不像Linux的gpu scheduler,是我们内核驱动开发人员自己实现的所以可以随意improve。后续有可能还是要研究windows上该如何做,有个变通方法暂时不表,因为该方法只有了解AMD 硬件的人才看得懂,因此没有太大意义在此处描述。

具体游戏测试FPS图

我用Nexuiz 这个OGL游戏做了一个对比测试,首先物理机跑下来是154 fps,然后我分别用round-robin和按需调度在4 VF下做了测试,结果如下:

round-robin下,4个VF 的avg fps分别为:76,79,73,78, total一起是306。

按需调度下,4个VF的avg fps分别为:94,95,120,98,total一起是407。

可以看到首先avg fps就提高了25%。

然后再放上三张图,分别是物理机,RR调度,以及按需调度下的FPS曲线图(RR调度取了FPS 79的那个VF 的图,按需调度去了FPS120的那个VF 的图)

物理机:

nexuiz-bm

Round-Robin 下的某VF:

nexuiz-bm

按需调度下的某VF:

nexuiz-bm

大家可以看到,按需调度的FPS曲线图,从曲线上看更接近物理机模式的图(这就是由于延迟被降低的缘故),就好像是物理机的fps曲线图在振幅上打了个折扣一样;而RR调度的图像方波图,即一高一低跳跃明显(当占有GPU时FPS会非常高,当其他VF占有时自己的桢会遭受延迟处理,因此FPS会下掉),因此,而且形状上也不太贴近物理机的曲线图。

理论上,每一帧的导数与物理机的每一帧的导数越接近说明延迟越少,以后我会考虑写脚本去做定量的数值分析来判断调度算法的质量,而不是目前这种靠肉眼观察fps曲线图的主观判断,定量分析算法如下:

prepare data

fps_bm = bare-metal.frames.avg;

for each “idx” in all frames -1

​ bare-metal.frames[idx].delta = bare-metal.frames[idx+1].fps - bare-metal.frames[idx].fps;

for each “X” in all VFs:

​ fps_VF[X] = VF[X].frames.avg;

​ scaling[X] = fps_bm/fps_VF[X];

​ for each “idx” in all frames -1

​ VF[X].frames[idx].delta = VF[X].frames[idx+1].fps - VF[X].frames[idx].fps;

Do the calculation for the score

for each “X” in all VFs:

​ for each “idx” in all frames - 1:

​ gap += square root of (bare-metal.frames[idx].delta- scaling[X] * VF[X].frames[idx].delta)^2

gap 就是最后的分数,理论上越小越好。

NOTES:判断质量的标准是图形是否足够接近物理机,如果不是用fps的阶梯而是fps本身的话,并不能充分反映”fps曲线形状“这个概念,请读者自行理解这里的区别。

这篇文章就到此结束,Farewell !

AMD显卡的硬件虚拟化核心功能---GPU抢占

发表于 2017-08-05

青云原创, GPU 虚拟化

转载请注明出处:https://navycloud.github.io/2017/08/05/gpu-preemption-in-virtualization/

本文将详细介绍AMD 显卡的GPU抢占功能以及在GPU虚拟化中的重要作用,理解GPU抢占需要适当的GPU原理基础,因此我也会适当笔墨部分基本的GPU Pipeline。

AMD GPU硬件虚拟化基本术语

由于本文并不是AMD GPU 硬件虚拟化基本原理教程,因此不会涉及基本原理方面的讲解,所以下面简单描述一下几个常见的术语。

  1. engine 指一种GPU 内部的计算资源,主要是指:GFX (graphics, 用于图像绘制),SDMA (用于数据搬运,复制),MM (多媒体,用于视频编解码)
  2. VF:指 Virtual Function, 也就是虚拟出来并pass through 给虚拟机的AMD pci device
  3. PF:指Physical Function, 也就是AMD GPU 硬件本尊的pci device
  4. world switch: 指对一个(或多个)engine进行调度切换,即将engines 从一个VF 调度给另一个VF使用, 这之中就涉及到抢占。
  5. Pipleline: 也就是GFX engine,即GPU渲染流水线,这是一个非常基本的GPU术语,各位可以google之,一大把资料,它就是游戏画面渲染的工厂,非常复杂,下面会简单描述。

什么是GFX pipeline

pipeline 是有多个模块组成的,多年来一直在不断地发展和变换,下面介绍一个典型的pipeline:

从这图(微软的DX11 pipeline)可以看到,pipeline发展到目前已经由很多环节组成了,但基本的模型并没有变:

  1. 首先需要游戏输入顶点数据(以及每个顶点附带的属性数据,例如法线,颜色,自定义数据等),然后vertex shader stage用游戏提供的”vertex shader” (shader是一种程序) 来处理每一个用户输入的顶点,是吞吐量非常高的SIMD计算方式。vertex shader具体做什么完全取决于游戏开发者怎么攥写shader程序本身,不过套路还是有的:例如把给定顶点从模型空间用矩阵转换为视角空间(具体参考计算机3d图形学),并结合此次具体渲染的对象做一些处理:比如渲染风中的树叶可以用一些随机数来小范围改变树叶上部分顶点位置,从而制造每一片树叶都在风中自然而又凌乱的婆娑效果,而不是步伐一致的抖动。(在可编程pipeline出现前,要达到这样的效果需要花费巨量的CPU运算)。

  2. Hull shader stage 是为tesselator 这个feature服务的,它将VS的输出分组,例如每三个顶点形成一个三角形,称为一个patch,然后将这些patch根据给附带的属性(每一个patch都有属性数据,例如复杂度就是一个数据)将一个patch分割成更多的patch并输出,当然也可以完全不做任何修改直接输出,完全取决于游戏开发人员。

  3. tesselator shader stage 是不可编程的,它的作用是根据之前的shader stage(Hull shader)产生的输出靠硬件“聪明”地自动衍生出更多的顶点,从而把模型变得更复杂(效果更好),AMD是第一个提出该概念的公司。

    (在OpenGL 规范中,由于tesselator shader stage对开发人员透明,不可编程。因此直接把hull shader stage 和 tesselator shader stage 合并成了一个stage,叫做TESS control shader stage)。

  4. domain shader stage 是让游戏开发人员用来将tesselator shader stage 输出的(大量的)顶点组成的patch(三角形,正方形,etc…) 的每一个顶点再转换为世界坐标系(tesselator shader stage 输出的patch的顶点坐标系是每一个patch各自的内部坐标系,因此这些顶点的x,y,z,w数值对世界坐标系无意义),亦包括这些顶点附带的属性值,例如它的法线向量,颜色,纹理坐标等。

  5. Geometry shader stage: 作用不大,它是把一个一个三角形转换为三角形带(strip,比如相邻的两个三角形不需要用6个顶点表示,只需要4个),当然这也是取决于游戏开发人员的意愿,你可以什么都不做,也没毛病因为domain shader stage 得到的数据已经可以给光栅化引擎了。

  6. Rasterizer stage (光栅化)就是把数学上的顶点坐标,以及附带的属性例如法线,颜色,纹理坐标,用户自定义参数等,根据屏幕分辨率用固定的算法把这些“数学”顶点映射到离散的屏幕上去,也就是像素。(数学上的顶点的位置坐标是浮点数,而屏幕的分辨率却是具体的整数,因此需要有个变换),它的输入是”数学“级别的三维空间的顶点坐标及其附带的各种数据,输出是二维平面的x,y坐标以及附带的数据(颜色,法线,深度值,模板值,等等)

  7. Pixel shader stage就是通过Rasterize 的结果把每一个frag作为一个数据(离散坐标,颜色,法线,各种附带属性)传递给 pixel shader(也是游戏自己撰写的程序)处理,最终产生Pixel, Pixel就是像素,也就是屏幕上看到的每一个由颜色的点。

实际上,还有clip rectangle, color buffer, depth buffer, stencil buffer等“非可编程”的硬件模块会参与到最终图像的计算,但是这些都不需要详细描述,因为他们都不是可编程pipeline的核部分心,更无关乎GPU抢占。

以上就是微软DX11定义的pipeline,vulkan和OpenGL也有各自的定义,但是大同小异,AMD和NV的可编程管线硬件也都能完全100%支持这三个API定义的流水线,实现就是靠驱动软件,我们称之位3d驱动(DX11驱动,ogl驱动,vulkan驱动)。OK,GFX pipeline的介绍到此为止,下面是正题。

GPU 抢占

下面介绍重点,GPU的抢占流程。

什么是GPU 抢占

CPU 抢占应该不少读者已经了解,简单说也就是CPU在内核态运行过程中,它会找机会主动停止自己的运行(例如spin lock和软中断,tasklet的完成时间点,具体我没有一一核实),从而可以去服务另一个线程,目的是让响应速度变快,缺点是耽误了内核态的运行,而且一次CPU抢占也对应一次task切换,从而对整体的系统性能是有负面影响的。

GPU 抢占和CPU抢占类似,也是把GPU在运行中尽快停止,并服务另一个客户(客户在虚拟化的应用就是VF,在非虚拟化环境中可以是另一个执行体,例如另一个更高优先级的gpu command)。

GPU抢占泛指所有GPU engine的抢占,不过一般而言我们指的是GFX engine。

AMD GPU硬件虚拟化为何需要GPU 抢占

AMD GPU硬件虚拟化是基于分时复用策略的,意味着任意时刻对任意engine只能有一个VF在占用它,因此必须要通过world switch这种分时复用的方法来让每一个VF都有机会使用engine资源。每一次把一个engine从一个VF切出去,就是一次engine preemption(抢占)。

如果没有GPU 抢占,那么当world switch请求发到某一个engine上(例如GFX)时,该engine并不会去暂停工作,而是待当前工作完全结束才去响应world switch请求,这就造成一个问题:如果当前工作量非常庞大,那么会让其他VF 等很长的时间导致差用户体验,而且影响engine分配的公平性。

PS:即使有了GPU 抢占,也只是缓解问题,因为AMD 目前的GPU 抢占(对GFX 和MM engine而言)并不能立刻完成,它能多块得完成还是依赖于工作量的大小,只不过它比不抢占而彻底等待工作完成要快得多。对于SDMA engine而言抢占可以做到非常快,SDMA仅仅是数据传输的工作,所以可以做到立刻暂停。

题外话:当有多个VF同时运行游戏时,按照目前默认的分时复用策略,在实际游戏中会造成忽快忽慢的卡顿(GPU latency)现象,这个是有严格分时复用引起的,我在另一篇文章“AMD GPU虚拟化的按需调度策略”中提出了一个新的调度策略可以很大程度减少卡顿现象。

GFX engine 的抢占

GFX 抢占的原理并不简单,但是由于AMD内部的技术细节我不能public发表,因此就只能从概念上描述一下:

  • 在GFX engine得到world switch请求时,这个请求指令是hypervisor通过programing其它硬件模块(RLCV,只需要知道名字就可以了,不必追究细节,我也不会透露)来通知CP 的,CP会负责GFX 抢占的所有工作。
  • CP是command processor缩写,它是GFX pipeline的前端预处理模块(也是一个firmware),负责执行3d驱动填入command stream的指令,这些列指令有些负责设设置寄存器,有些负责同步信号量,有些负责读写内存,有些些是配置一些pipeline需要用到的资源等等。当CP处理到DRAW这个指令时就会给pipeline发送信号驱动pipeline开始工作,一旦CP收到rlcv的抢占请求时,CP就会在任意“可以抢占的指令”上发起抢占工作。抢占的主要工作内容就是保存当时的GPU GFX 上下文现场,具体如何保存,保存哪些,就不便展开了。抢占完成后,RLCV会通知SDMA去做抢占,全部完成后,RLCV会执行word switch,它也会保存另一部分寄存器,然后把GFX和SDMA切换到另一个VF上,也即是恢复另一个VF之前被抢占的工作。 整个world switch的流程可以简单描述为:
    1. RLCV发起抢占请求
    2. CP 收到请求,对gfx发起抢占,等成功后保存gfx的上下文资源,寄存器等
    3. SDMA收到请求,自我暂停并保存SDMA上下文
    4. RLCV待CP,SDMA都完成抢占后,根据一个SaveRestoreList对一些寄存器做保存处理(这些寄存器不会由CP和SDMA负责保存)
    5. RLCV会把需要恢复的VF的寄存器从该VF 的SaveRestoreList中恢复到VF中。
    6. RLCV会把CP,SDMA调度到该VF
    7. RLCV通知CP,SDMA去恢复之前被抢占的VF
  • CP 通过一个叫DRAW的指令驱动GFX Pipeline工作,而pipeline一旦开始工作是不能停止的,必须等到pixel shader stage完成了才能停止,因此CP只能在每一次DRAW之后暂停GFX ENGINE,通常而言,3d驱动会在一次command stream中要求gfx engine做几百个或者上千上万个DRAW 操作,也就有那么多个时间点可以允许CP暂停使用pipeline,也就达到了所谓的抢占的效果,可以说目前AMD的抢占是比较粗粒度的。一次DRAW 的工作量有多大,就意味着抢占GFX需要等待多少时间才能完成,而DRAW的工作量取决于大致上两个指标:
    1. 一次DRAW指令会用到多少顶点,越多则shader engine工作也越多,这是3d驱动控制 的。
    2. 整个pipeline的各个shader写得有多复杂,极端情况如果某个shader是一个死循环,那就永远抢占不了,这种情况下GPU就死了,所有VF都不能工作了,必须要通过TDR得方法来拯救整个GPU。(关于TDR, 也即Time out Detection and Recovery,是我于2016年为北美云客户亚马逊在Linux AMD 虚拟化驱动上开发的一个feature,此前AMD 的Linux内核驱动乃至整个linux 中的所有各厂商upstreaming的显卡驱动都没有TDR 这个功能,我会抽空另写一篇描述TDR)。

NOTE:

  1. DRAW一旦开始不能被抢占,只能等他完成,因此一个含有1000个DRAW的command stream中理论上有1000个点可以被抢占。
  2. 在DRAW之前,command stream中会有设置各类shader程序的指令,用以告诉GFX ENGINE当pipeline开始工作时,各类shader指令在哪段内存中。

SDMA engine 的抢占

这就比较简单了,概念上仍旧是收到RLCV的抢占消息后最快速度的保存,下次再恢复,但是SDMA由于硬件实现简单因此可以做到立刻抢占。

multi-media engine 的抢占

这块比较复杂,但是远远没有GFX复杂,基本上也是粗粒度的暂停,保存,恢复操作。区别是mm的抢占发起方不是RLCV,而是一个叫MM-SCHEDULER的模块,不过本质上没有区别啦。

本文到此结束,Farewell!

navycloud

navycloud

2 日志
2 标签
© 2018 navycloud
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4