最近,小H工作地方食堂新增了一个窗口,卖小龙虾尾盖浇饭,在高峰时段排长队。小H在体验过几次痛苦的排队以后,决定悄摸摸地提前去吃小龙虾尾盖浇饭。
离12点还差20分钟的时候,小H起身,发现自己老板在座位上。小H有点不好意思去,但仔细一看,自己老板在一个会议里面和其他人对喷,同时还在打《王者荣耀》,于是放心大胆地去食堂了。
由于小H采用了错峰的方式,大大减少了为了享用小龙虾饭而浪费的时间。小H蹦蹦跳跳地去跟方老师显摆:
1. 发现了错峰减少排小龙虾饭时间的窍门;
2. 原来可以玩《王者荣耀》不耽误开会;
方老师笑了一会儿,告诉小H:其实在前几期里面遗留的一些问题,从这里面都可以找到答案!
在上一期,我们遗留的一个问题是,为什么GPU的寄存器列设计得那么大,每个SM的一个象限要分配16K个寄存器。
这个问题就要从小H的老板如何一边开会一边打游戏说起。
我们知道,人脑是有一心二用的潜能的,而在多核处理器出现之前,操作系统也可以将一个物理的CPU核心给多个进程使用,这一机制叫做“上下文切换”,也就是在需要把A任务切换到B任务的时候,执行这一序列的操作:
1. 产生中断/系统异常,在中断/系统异常处理例程里面,把A任务的上下文保存到内存中的堆栈(stack);
2. 找到B任务的堆栈,把堆栈中保存的上下文内容恢复;
3. 根据B任务的上下文内容恢复B任务的执行。
所谓的“上下文”主要包括:
1. 指令指针(Instruction Pointer)寄存器,它指示当前执行到哪一条指令。在恢复指令指针寄存器以后,就可以继续执行原有任务;
2. 其他通用寄存器,包括算术寄存器、各类地址基址和偏移量指针寄存器、堆栈指针寄存器等;
3. 含有各类标志位的状态寄存器;
对于多核多线程的CPU,每个硬件线程进行任务切换,都会涉及到保存现场的这一操作。我们知道,GPU内部的硬件线程数量比CPU又多出2个量级,以NVidia H100为例,它的每个SM有32个线程,而整卡有132个SM。在GPU进行任务上下文切换的时候,如果把每个线程的寄存器上下文全部保存到内存堆栈,会造成很大的代价(cost)。
NVidia的工程师们想出来的解决方案就是:将寄存器列划分为若干个块,所有任务的上下文都保存在寄存器里面,根本不做从寄存器到内存的搬运,上下文切换只是使用不同的寄存器块来实现。
那么,为什么GPU要进行上下文切换呢?
我们知道,GPU的内存也是DDR动态内存,虽然使用了HBM3等高速内存接口,但本质上还是动态内存,因此,内存事务的速度远低于计算,等待内存事务完成,会让GPU处于闲置状态。
因此,在SM中会对warp进行调度,让warp在不同的上下文之间切换,当warp中的线程阻塞在等待内存事务的时候,调度器就会向线程发射其他的任务的指令,让这些线程执行其他任务。在kernel函数的编译阶段,编译器会考虑到这一问题,计算出每个内核线程所需要的寄存器数量(寄存器总数/warp数量/每个warp的线程数量)。只要所有的线程块都具有相同的大小,并拥有已知数目的线程,每个线程块需要的寄存器数目也就是已知和固定的。这样,GPU就能为在硬件上调度的线程块分配固定数目的寄存器组。
每个GPU芯片上,除了寄存器组,还拥有SM内的Shared memory和芯片内部的L2 Cache。我们将在后面详解它们的组织方式。