1.背景介绍
由于 FPGA 具备可编程和高性能计算的特点,基于FPGA硬件的AI计算加速,正广泛地应用到计算机视觉处理领域。其中极具代表性的部署方式之一就是使用FPGA和CPU组合构成异构计算系统,并在CPU上搭载Linux操作系统,运行AI推理引擎框架及视频图片处理等各种业务。其中,如何协调CPU和FPGA的计算关系,成为这套异构系统的关键,而这部分关键技术则是由驱动系统来完成的。
协调CPU和FPGA这对异构兄弟的计算关系,可以有许多模式,根据不同的应用场景会有很大的变化。一般在端侧,整个系统的应用比较单一可控,较少考虑并发及虚拟的情形。根据这一情况,EdgeBoard应运而生。
2.EdgeBoard介绍
EdgeBoard是百度打造的基于FPGA的嵌入式AI解决方案及基于此方案实现的系列硬件。作为端侧的解决方案,没有在PL侧为FPGA设计专用的内存,而是采用了PS和PL侧共用DDR内存的结构。因此,对这种异构系统的CPU和FPGA的协调就落在了内存的管理方面,这就是驱动系统中关于内存管理的子系统。
3.全文概述
本文将重点介绍EdgeBoard中关于CPU和FPGA内存驱动设计的关键技术。下图是整体EdgeBoard的软件框架图,其中内存驱动位于内核部分。下面将从内存的分配、释放、回收等方面来逐步介绍。
4.内存的特点和FPGA的内存需求
在FPGA芯片PS侧,CPU使用多级缓存来访问DDR,在Linux操作系统中,使用内存映射页面(通过页表来管理),而对DDR的物理连续性没有要求,它们被映射到虚拟连续的地址空间中。而在PL侧的FPGA一般未使用任何缓存机制,它们在计算中都是直接访问DDR。每一次读写操作都是读或写一个连续的内存空间,而且要求这片内存的起始地址要对齐在一个特定的地址偏移处(偏移0x10)。一次计算中的多次读写都要求访问的DDR是一致连续的。
针对CPU和FPGA这样的内存需求,我们设计Linux驱动内存的子系统时,就要充分考虑到:1)cache的影响;2)FPGA使用内存的物理连续性;3)传递给FPGA使用的内存块要满足偏移要求(segment alignments)。
5.保留系统内存
针对上述需求,我们采用分隔物理内存的设计方案,从整体系统中保留其中的一部分内存,让Linux只使用另一部分内存。这部分被保留的内存,分配权归内存驱动,驱动从保留部分中分配的内存块在Linux系统中和FPGA系统中都可以访问。在此期间,分配时要保证物理连续和起始偏移特性,使其能满足FPGA的需求。
在EdgeBoard实践中,我们采用的是Xilinx的ZynqMP系列的FPGA芯片,使用PetaLinux工具链来编译Linux内核,并采用DeviceTree中的reserved-memory节点来实现内存的保留。例如,系统总体2GB内存,保留1GB给FPGA,留下1GB给Linux操作系统,DeviceTree中相关节点的设定就是这样的:
下面就从驱动设计实现的各个方面介绍。
6.初始化内存的内部映射
FPGA总体设备驱动是采用字符设备platform driver的形式来编写的:在Device的probe阶段,对驱动内所保留的内存块做好内存映射(memremap),并使用合理的数据结构,保存好各个参数,以供后续使用。例如:
mem_start、mem_end、base_addr 等结构成员的定义如下:
7.内存的分配
关于内存的分配,采用了mmap调用的方式:在FPGA设备的初始化期间,初始化字符设备时传递了file_operations结构变量,这个结构变量的mmap指针初始化为我们的内存分配函数。
内存分配中,我们使用了内核提供的bitmap数据结构,来管理我们保留的内存区域——bitmap位数组中的每一位代表着16k的一个内存块,另外还使用相同长度的数组来管理内存被分配的客户owner(即file指针)、内存分配的块的数量等信息。
另外,在分配的内存对应的vma(VMM Memory Area)中,我们还注册了自己的私有数据private data来记录对应内存的必要信息:如对应内存块的总线物理地址范围和映射地址、bitmap位数组的索引等,用于地址转换;再如对应内存块的一些finger信息,用来标识保留内存块。
同时,我们也为VMA的vm_ops注册了close函数,用于为对该块内存的回收做好准备。
关于内存分配的代码量较大,这里就不一一列举了。
8.内存的回收
已分配出去的内存块都需要回收,其中有两种最具代表性的情况,一种是用户release一块内存的处理,另一种则是用户关闭设备时对未release的内存块的清理回收。
当用户release一个内存块时,对应的vma的close函数会被自动执行。为此,我们注册这个函数作为内存释放处理函数:函数实现中,首先检测private data避免错误处理,然后恢复对应的bitmap位数组中的位信息,清理owner和块数量信息,使得这块保留内存又回到了待分配的状态。
当用户关闭设备时,会调用设备注册的release函数。因此,在设备的release函数中,我们遍历owner数组来清理owner与设备的file指针相同的内存块,以此达到批量回收的目的。
9.内存的地址转换
内存的地址转换,是完成总线物理地址和虚拟逻辑地址的双向转换,为内存cache的flush/invalidate、以及交由FPGA之时要进行的处理等提供支撑。系统也通过两个IOCTL向用户层暴露这两个转换操作。
地址转换操作中,首先找到对应的vma,然后计算出offset,最后检查vma对应的标识是不是保留的内存块——如果是,就使用我们vma保存的private_data中的信息及offset完成相关的计算。
10.内存缓存的flush和invalidate操作
在CPU及其异构兄弟FPGA之间,使用DDR内存来传送数据时,需要使用flush或invalidate来消除对应缓存的影响。
首先,我们的驱动代码是运行在CPU上面的,当针对一片内存的处理从CPU转移到FPGA之前,需要对这片内存的cache执行flush操作,使得内存cache中的所有改动都写入到DDR,然后FPGA开始对它的处理工作。其次,当针对一片内存的处理从FPGA转移到CPU之前,需要对这片内存的cache执行invalidate操作,使得cache内容无效,下次CPU直接读取即从DDR中去主动load从而刷新cache。这样,CPU就可以很好地跟FPGA相处了。
对于flush和invalidate操作,则是CPU体系相关的。EdgeBoard使用的是A53v8(对应于AArch64执行集),cache flush和invalidate的代码如下:
11.内存驱动中其它功能设计及考虑
上文中提到的保留内存区域中每个保留内存块的大小是16K,其实这里是可以启用多规格的,这样用户在使用上就会更加方便,用途也会更多。但是关于内存管理部分就需要更多的数据来进行管理了。
另外,我们可以通过建立一些IOCTL的方式为不同进程之间的内存共享做出一些快速、简洁的方案,这也是我们可以在内存驱动中设计考虑的。