AI的端侧应用离不开各种算力平台,目前形成了CPU+NPU,CPU+FPGA以及CPU+GPU的几类算力搭配,他们各有优势,也各有弱点。先看它们的共同点,从组合名字可以看出,那就是他们都依赖CPU,这是因为无论NPU,FPGA还是GPU,它们的架构特点决定了在流水线pipeline,逻辑控制方面要弱于CPU,它们要么作为专用算力加速器,没有完备的指令集系统,比如GPU有简单的流水线,可以完成取指,解码执行的操作,它的流水线适合做大量的SIMD并行数据计算,比如DP4a指令(4元素矢量点积),Intel、AMD、NVIDIA基本所有的GPU都支持它,但是无法完成复杂的执行流以及逻辑控制。另外还有一些没有取指流水等机制,无法自举执行指令,必须依赖CPU打配合,有点类似于FPU或者NEON单元,比如NPU就是这个样子,这类算力无法独立完成取指令执行的操作,运行依赖于CPU不断的喂指令,在设计中,必须配合CPU一起用。
关于流水线独立这一点,就连谷歌的TPU也不例外,TPU类似于FPU,而非可以自行取指令的GPU就是证明:
之前对与RISCV Vector指令,MIPS的FPU以及ARM的NEON都有过一些分析,但当时对AI没有太多概念,对AI如何使用这些单元提供的算力缺乏认识,分析完了感觉没有结果。现在对AI有了一些了解,尤其其中的算子实现对异构算力的设计需求有了一些自己的看法,这样反过来从需求方倒推,对ISA指令集的设计理解的会更加深刻。 所以觉得有必要结合AI,对一些主流的CPU ISA指令集特点做一个总结,包括最早接触的MIPS,Xlinux(FPGA)做过验证,之后是ARM,然后RISCV和DSP(Xtensa),ARM和RISCV也都跑过Altera FPGA。这里面RISCV是最精简的,软件上可以认为是MIPS的一个子集(可能Vector向量指令的区别会大一些),可以放在一起分析,感觉这是一个比较大的主题和工作量,万事开头难,先从重温MIPS开始,之后逐渐切入其它ISA,横向对比,纵向深挖,分析它们各自对AI加速提供的各类ISA级的机制以及背后的思考,个人的一些看法,可能不对,期待被打脸。
首先从搭建MIPS开发环境说起
MIPS编译工具链下载
MIPS官网:MIPS
比较滑稽的是,在MIPS官网主页上,一幅标题赫然写着:
We’re taking RISC-V to new heights(我们将RISC-V推向新的高度),以MIPS多年积累的技术实力,相信这个新“高度”不难实现。但是市场是残酷的,只见新人笑,哪闻旧人哭,曾经的产业一哥如今沦落到要为新近晚辈执鞭随镫,不禁让人喟叹。
MIPS不但是这样说的,还是这样做的,MIPS已经发布它的的首颗RISCV ISA CPU IP eVocore。
更奇葩的是MIPS官网的副标题竟然是elevating riscv,几年前MIPS就已经宣布放弃MIPS拥抱RISCV,一代旗手沦落至此,令人唏嘘。
顺着导航,来到Developer Tools – MIPS,可以看到开发工具和开发模式。
现在有多没落,曾经就有多辉煌,从高端的64位MIPS服务器,到低端32位MCU级控制器,都可以看到MIPS的身影。
之后来到编译器下载链接:MIPS Compilers – MIPS
MIPS支持LLVM工具开发,不过我们这里用GCC
我们将bare metal和Linux用户态工具链都下载下来,由于我们要编译的是Linux内核,究竟使用哪种工具链关系不大,任何一个都可以,我们解压:Codescape.GNU.Tools.Package.2019.02-05.for.MIPS.IMG.Linux.CentOS-6.x86_64.tar.gz
之后导出环境变量:
export PATH=$PATH:/home/caozilong/mips/mips-img-linux-gnu/2019.02-05/bin
之后,在任何一个目录,都可以直接输入mips-img-linux-gnu-gcc访问GCC了。
下载最新的Linux内核代码:
从kernel.org下载并解压:
编译
make ARCH=mips CROSS_COMPILE=mips-img-linux-gnu- malta_defconfig
make ARCH=mips CROSS_COMPILE=mips-img-linux-gnu- menuconfig
make -j4 ARCH=mips CROSS_COMPILE=mips-img-linux-gnu-
编译结果:
反编译
mips-img-linux-gnu-objdump -d vmlinux>vmlinux.dis
从运行地址也可以看出,内核虚拟地址从0X80000000地址开始,这也是MIPS ISA地址分布的特征。
32位MIPS处理器将地址空间划分为四段,从低地址到搞地址,各段的特性如下:
1.用户模式,监管模式和核心模式都可以访问,用户模式下只能访问该段空间,需要通过MMU映射,其CACHE算法由页表中相应字段规定,在LINUX操作系统中,该段存放用户程序,动态连接库,程序堆栈等等。
2.kseg0,只能由核心模式访问,该段地址不通过MMU进行地址映射,其CACHE算法由CONFIG控制寄存器的K0域决定,KSEG0直接映射至低512MB物理地址空间,映射方法为抹去最高三位,在LINUX 系统中,该段用来存放内核代码,数据,异常入口。
3.kseg1,只能由和新模式访问,该段地址不通过MMU进行地址映射,也不通过CACHE进行缓存,KSEG1同样映射至低512M物理地址空间,映射方法为抹去高3位,KSEG1和KSEG0映射到相同的物理地址,但KSEG1的访问无需依赖CACHE,因此MIPS处理器程序计数器的复位地址0xbfc00000就落在KSEG1段,完成CACHE初始化等工作后,在跳转到KSEG0执行,可以利用CACHE提高访问速度,在LINUX操作系统中,该段存放启动ROM和IO寄存器等。
4.kseg2,由于核心模式和监管模式访问,通过MMU进行映射,CACHE 算法由页表决定,LINUX操作系统中,该段用于存放动态分配的内核数据。
编译完成,下一步准备用busybox构建一个小的文件系统,之后用qemu模拟器进行启动,就可以愉快的跑MIPS ISA了。
MIPS处理器复位
处理器的第一条指令,实际上是由复位信号控制的,但受限于各种其他因素,复位信号并没有对处理器内部的所有部分进行控制,例如TLB, CACHE等等。
MIPS处理器复位后的第一条指令将固定从地址0xbfc00000位置获取,这个过程是由处理器执行指针寄存器,也就是PC,被硬件复位为0xbfc00000而决定的。
对于MIPS来说,0xbfc00000这个虚拟地址属于直接映射且不经CACHE缓存的地址段,直接对应于物理地址的0x1fc00000,正是因为这个地址是直接映射的,不需要使用TLB转换,也不经过CACHE缓存,不需要使用CACHE结构,所以硬件上没有必要对TLB,CACHE这两个结构进行初始化,只需由硬件提供机制,软件在启动过程中在对其进行初始化即可。
环境搭建写到这里,下面简要分析一下MIPS版的Linux内核,从调度入手:
save_some会调用get_saved_sp从kernelsp结构中获取current对应的内核栈指针,而且是在 判断prev状态为user mode的时候才会这样做,这说明什么? 说明从用户空间通过系统调用进来的时刻,内核栈一定是空的,等待新来的客人。但在内核中睡眠的用户态线程,当前处于内核态,它们不需要这样的方式获取栈,判断依据是CP0_STATUS寄存器的CU0 BIT。
arch/mips/kernel/r4k_switch.S,相当于arm下的__switch_to,kernelsp维护者当前进程的内核态堆栈指针,铁打的营盘,流水的兵,任何用户态进程需要内核服务的时候,一猛子扎下来都要嫖一下kernelsp,发生点故事后再退回去,不留下一点云彩,所以用户态resume进来的时候,内核栈一定是空的,注意系统调用而非中断。
arch/mips/kernel/scall32-o32.S,相当于arm下的SVC异常入口,STI开中断,是宏定义。
安装异常处理向量入口:
kernelsp的定义:
页目录基址寄存器
MIPS区别于ARM,RISCV和X86架构的一个重要不同点是,MIPS没有页目录基址寄存器,就拿其他三种架构来说,ARM有TTBR0,TTBR1寄存器,RISCV有SATP寄存器,X86有CR3寄存器。除了MIPS之外,还有另外一种架构也没有硬件基址寄存器,它就是OpenRISC架构。
我们拿出证据,关键就在switch_mm的实现:
ARM,check_and_switch_context将会进行TTBRX的设置。
X86,switch mm的调用栈将会调用write_cr3将新的页目录设置进硬件寄存器。
RISCV的switch_mm实现更直接,就在函数体中直接设置了SATB寄存器。
反观MIPS和openRISC:
MIPS:
openrisc: 两种架构都没有全局寄存器来存储PGD。
为什么呢?仔细分析这两类架构,我们可以看到两类架构的明显区别,MIPS和OPENRISC都是TLB软件重填的,而其他架构都是硬件重填TLB。
由于需要硬件来重填TLB,所以,必须要开出硬件寄存器,让硬件可以根据寄存器的信息去做page walk,但是软件重填就不需要了,记录在一个某个地方,在TLBREFILL 异常中访问这个位置就可以了。
从图中代码中可以看到,openRISC将下个进程的PGD放进了current_pgd这个每CPU变量中(TTBR和CR3以及SATP也都是每CPU寄存器),而在TLBREFILL异常中,将会读取current_pgd,获取当前页目录基地址
dtlb refill handler:
itlb refill hander:
MIPS 遵循同样的套路,在switch_mm中将下个进程的PGD记录在内存中:
和OPENRISC不同的是MIPS的TLB重填函数是由函数生成的。
关于TLB软硬件refill和基址寄存器的内在逻辑,请教了社区MIPS专家,理解大抵是对的。
而pgd_current,恰好就是switch_mm函数中,调用的tlbmiss_handler_setup_pgd函数设置的,只是比较隐晦的是,tlbmiss_handler_setup_pgd也是由代码动态生成的。至此,逻辑闭环。
MIPS ASID:
用一个不太恰当的比喻,任务切换的过程就是给CPU“洗脑”的过程。既然是洗脑,下一个执行的任务就要从头开始记忆,是不是之前的记忆都没有用了呢,显然不是,有的时候,具备记忆移植或者迁移的能力的CPU,执行效率会更高,ASID就是做这个的。MIPS的ASID空间只有8位,也就是允许同时256个进程共享TLB,如果发生overflow,就需要刷新TLB开始新的cycle.
处理ASID overflow的数据结构,每个version表示一个asid cycle。
ASID切换逻辑的cmodel仿真代码:
#include <stdio.h>
#include <stdlib.h>
unsigned int cpu_cache = 0;
int flush_tlb_cnt = 0;
void switch_mm(unsigned int next_id)
{
if((next_id ^ cpu_cache) & 0xffffff00)
{
if(!((cpu_cache += 256)&0x000000ff))
{
printf("%s line %d, flush tlb %d times.\n", __func__, __LINE__, ++ flush_tlb_cnt);
}
next_id = cpu_cache;
}
printf("%s line %d. next_id = 0x%08x.\n", __func__, __LINE__, next_id);
}
int main(void)
{
unsigned int next_id = 0;
for (next_id = 0; next_id < 1000; next_id ++)
switch_mm(next_id);
switch_mm(666);
return 0;
}
上面说的PTW(Page Table Walker)应该就是这里的了吧,HTW官方文档表示hardware table walker,表示硬件页表遍历器。
所以较新的MIPS也是有存放PGD的硬件寄存器的。