算子计算与调度杂谈
算子计算与调度杂谈
9.4.1 GPU内核驱动分析概述
不同CPU相比,GPU中包含了大量的并行计算单元,适合处理像素,矩阵,坐标等大量同类型的数据,因此,很多LINUX上的应用程序为了能够利用GPU的加速功能,都试图和GPU直接打交道,因此,系统中可能有多个组件或者程序同时使用GPU,比如桌面系统中OpenGL的实现MESA。这样会带来一个问题,就是多个进程并发访问GPU,如果控制不好,势必会造成系统工作不稳定,为了解决这样的问题,LINUX内核开发者在内核中设计了DRM模块,所有访问GPU的操作都通过DRM统一管理,由DRM来统一协调对GPU的访问,所以,在Linux系统中,各类GPU驱动,包括NVIDIA,AMD等大厂的GPU驱动都是集成在DRM框架中的。如图9.4.1所示,以AMD GPU驱动为例,AMD GPU的内核驱动结构上是一个复杂的软件堆栈,负责管理和控制AMD图形硬件,以便应用程序可以与GPU进行通信并利用其图形处理能力,这些能力都是通过DRM提供的,DRM屏蔽了不同显卡厂家的硬件差异,以统一一致的接口向上层提供服务。
图9.4.1 GPU的内核驱动结构
以下是AMD GPU内核驱动的主要组成部分:
1)硬件抽象层(HAL)
HAL是位于底层的部分,负责与GPU硬件之间的通信。它提供了一个抽象的接口,使上层的驱动和应用程序可以访问GPU的功能和寄存器,而不需要了解具体的硬件细节。
2)图形命令处理器(GCP)
GCP负责接收、解析和执行应用程序发送的图形命令。它将应用程序的图形请求翻译成GPU硬件可以理解的指令,以执行绘图操作和渲染任务。
3)AMDGPU内核驱动
这是AMD GPU驱动的核心部分,运行在操作系统的内核空间。它与硬件通信,管理GPU资源(如显存和显卡寄存器),执行调度和任务分配,以确保多个任务在GPU上正确运行。
4)GPU用户空间驱动
GPU用户空间驱动是运行在操作系统的用户空间的组件,它与AMDGPU内核驱动协同工作,提供对GPU的高级控制。它包括OpenGL和Vulkan驱动程序,以及OpenCL运行时库,允许应用程序与GPU进行图形和通用计算任务的交互。
5)AMDGPU-PRO
AMDGPU-PRO是AMD的专业级GPU驱动,主要用于支持专业应用程序和工作负载,如CAD、3D建模和科学计算。它提供了更丰富的特性和支持,包括对专业图形API和库的更好的兼容性。
6)AMD ROCm
AMD ROCm(Radeon Open Compute)是一个开源的GPU计算平台,旨在支持GPU加速的深度学习和高性能计算工作负载。它包括ROCm内核驱动和ROCm用户空间工具,为GPU计算提供了强大的支持。
总之,AMD GPU的内核驱动结构是一个多层次的系统,由硬件抽象层、内核驱动、用户空间驱动和专业级组件组成,以便应用程序可以有效地利用AMD GPU的图形和计算能力。这些组件共同协作,确保GPU能够执行各种图形和计算任务。
如图9.4.2所示,箭头表示源模块会调用箭头指向的目标模块中的函数或者符号,这种调用通常和LINUXN内核中的框架驱动注册相对应,即由架构中低层次的模块向框架中高层次的模块注册操作句炳,所以实际的架构层次应该是上下颠倒的,也就是说,从架构上看,应该是DRM在上,而AMDGPU在最底层,只有扇入口,没有扇出口。
图9.4.2 GPU与DRM间数据交互的内核驱动结构
9.4.2 GPU任务调度内核态分析
1.简介
内核态的GPU驱动需要处理用户态驱动下发的渲染命令,对于每个用户态的进程,在提交渲染命令前首先通过mesa驱动创建属于自己的上下文,然后往上下文关联的cmdbuf中填入渲染命令然后下发。渲染命令并不是一条一条下发给内核,而是批量统一放到一个内存chunk中,这个chunk内存空间是用户态已经向内核申请好的,由内核DRM框架管理,因此用户态下发的实际动作就是下发ioctl命令字然后把chunk的指针告诉内核,内核只要获取到这个地址将其放到内核的IB对象中就可以了。
2.GPU调度示意图
GPU调度示意图,如图9.4.3所示。每个应用程序下发渲染命令前先创建自己的渲染上下文,然后下发渲染命令,渲染命令到达mesa驱动层后,mesa会对应地找到该命令所属的GPU硬件模块,然后下发ioctl命令将渲染命令提交到对应IP的令牌缓冲器上。
图9.4.3 GPU调度内核结构
在图9.4.3中,渲染命令在下发到内核之后会被封装成一个job,然后找到其所属的渲染上下文,更具体地,找到该job所属的GPU IP的令牌缓存区,获取该job应该加入的调度实体,然后加入调度实体上的调度队列。一个job代表一个应用程序下发的渲染命令,不同job可能来自不同的应用程序。
3.数据结构组织图
数据结构组织图,如图9.4.4所示。
图9.4.4 数据结构组织图
9.4.3 GPU 内核驱动分析同步工作模型
在Linux内核的AMDGPU驱动实现中,dma-fence扮演着重要角色,AMDGPU的渲染/解码操作可能涉及到多个方面同时引用缓存区的情况,以渲染/视频解码场景为例,应用将渲染/解码命令写入和GPU共享的缓存区之后,需要将任务提交给GPU的运行队列等待执行,这个时候,应用不仅要监听缓存区中任务的执行情况,还要保证在任务执行完毕之前,不能再去修改缓存区中的数据。而AMDGPU也需要排它性地执行缓存区命令。在GPU执行结束,应用希望及时得到执行完的信息,以便回收缓存区重新利用,通知一般由绑定到某个缓存区上的中断完成。这些操作,如果使用经典的共享缓存区+锁的方式进行保护和同步,不但效率十分低下,而且各类内核机制杂糅在一起,缺乏一个统一的管理框架,使代码难以维护。
dma-fence则提供了一套简单便捷的机框架,将原子操作,休眠唤醒,同步/异步事件通知等功能嵌入到各种类型的缓存区管理框架中,将各类机制有机的结合在一起,减少了用户态空间的忙等,使缓存区使用更加智能高效。
以AMDGPU解码视频为例,利用dma-fence令牌缓冲器隔离了应用解码请求和解码任务任务本身,提交任务和解码完成通知均通过缓存区绑定的dma-fence进行交互,如图9.4.5所示。
图9.4.5 AMDGPU解码视频中dma-fence令牌缓冲器结构
为了便于分析,把图9.4.5内核中AMDGPU这部分的实现提取出来,写了一个可以独立加载执行的内核模块demo,类似于CMODEL,任务提交上下文和解码完成通知上下文在AMDGPU驱动中分别用内核线程和中断实现,在demo中则全部用内核线程实现。
工作模型,如9.4.6所示。
图9.4.6 独立加载执行的内核模块demo结构
栅栏阵列有256个slot,每个位置代表一个围栏。构成一个令牌缓存区。sync_seq为写指针序号,sync_seq只增不减,sync_seq mod 256为在array中的index,相当于令牌缓存区的写指针。
last_seq代表当前已经消耗掉结束位置(已完成处理的和fence绑定的缓存区)的序号,同sync_seq一样,它也是单调递增的自然序列,last_seq mod 256 为在array中的index,相当于令牌缓存区的读指针。
而fence_seq则表示写位置的一个抽样,这是一个在AMDGPU中的硬件令牌是所有进程共享的,每个进程随时可能会提交新的渲染任务,所以sync_seq时刻在更新,不适合处理。fence_seq则是某个时间点的快照,在last_seq和fence_seq之间的缓存区(fence),都会在下一批中一并得到信号。
当提交过快,但是消耗较慢时,写方追上读方,此时需要进行生产-消费之间的同步,读端将会反压到写端,写端调用dma_fence_wait进行同步。
9.4.4 GPU任务调度机制
1.GPU围栏
AMD GPU的围栏机制用来产生一个GPU的事件,用于CPU与GPU之间数据同步。当CPU发送一个fence到GPU某个IP的令牌缓存区后,GPU IP硬件会刷新该令牌上的cache。围栏机制可以用来保证用户态程序下发的渲染命令被顺序执行,从而保证上层应用程序渲染相关数据的一致性。实现围栏机制的硬件基础是AMD GPU提供的EOP(end-of-pipe事件)渲染命令,它和普通的渲染命令使用方法相同,作为渲染命令提交到令牌缓存区,然后被GPU执行。
2.命令格式
CPU与GPU通过令牌缓存区实现渲染命令的提交,渲染命令作为一个数据包被提交到令牌缓存区上,AMD规定数据包的格式,它被分为两部分,数据头和IT_BODY(information body),EOP渲染命令也有如下通用格式,如图9.4.7所示。
图9.4.7 GPU调度命令格式
数据包的头最高2bit代表数据包的类型,AMD将数据包分为了3类,分别是0,2,3。每类数据包的头部除了高2bit代表类型以外其它字段与数据包类型相关,其中type-0和type-2是用作写寄存器的数据包,type-3用做发送渲染命令以及一些特殊操作的命令,比如IB和Fence操作。type-3 数据包的格式,如图9.4.8所示。
图9.4.8 type-3数据包的格式
在图9.4.8中,数据头由以下4个字段组成:
1)PREDICATE:预测码。
2)IT_OPCODE:操作码,用于描述具体的渲染命令类型或者特殊操作,比如间接缓冲区和事件触发操作。
3)COUNT:描述IT_BODY的大小的字段,4字节为单位。它的值为IT_BODY长度减1。
4)TYPE:数据包类型,这里是3。