工欲善其事,必先利其器,在DNN库算子设计之前,首先要了解CPU的理论硬件性能。cpufp就是一款可以测出实际理论性能的工具。
cpufp的github链接:https://github.com/pigirons/cpufp。
架构分析
在分析cpufp之前,先介绍下浮点峰值计算。浮点峰值计算,一般是计算单位时间内,乘法和加法的最大总吞吐量,单位是GFLOPS或者TFLOPS,表示每秒钟计算乘法和加法的总次数。乘法和加法可能涉及到的指令包括:单独的乘法指令,如vmulps;单独的加法指令,如vaddps;融合乘加指令,如vfmadd231ps等。后者将乘法和加法融合为一条指令,在多数处理器中,三种指令都可以在一个发射端口每周期发射一条,所以乘加指令往往比单独使用乘法或者加法带来翻倍的吞吐量。
先来看x86-64,Intel在2010年推出Sandy Bridge架构(下面简称SNB),首次引入了256位宽的向量指令集AVX,即一条指令可以同时操作8组32位宽的数据类型。SNB架构示意图如下
六个dispatch ports,其中port0和port1各有一条向量乘法(256-FP MUL)和向量加法(256-FP Add),即一个周期内,SNB架构可以吞吐一条浮点向量乘法和浮点向量加法。由于AVX指令集还不支持融合乘加FMA,浮点峰值计算只能使用这两条指令的总和吞吐量。
综上所述,SNB架构的理论浮点峰值就等于(8Mul + 8Add) * 核心频率 * 核心数。例如SNB桌面高端的i7 2600k,有四个核心,关闭睿频后每核心频率3.4GHz,所以这款CPU的理论浮点峰值(关闭睿频)就是(8+8)*3.4*4=217.6GFLOPS。
如果想写一个小程序实测的话,可以这样设计:设置一个简单的循环,次数足够多,保证每次循环执行一个时钟周期,发射两条无依赖的vmulps和vaddps指令,cpufp就是这样设计的:
.loop:
vmulps %ymm2, %ymm2, %ymm1
vaddps %ymm4, %ymm4, %ymm3
sub $0x1, %rax
jne .loop
.loop是循环体;前两条vmulps和vaddps指令,输入和结果使用不同的寄存器,这样前后两个相邻循环的同一条指令产生WAW型寄存器依赖,通过寄存器renamer部件可以解决;然后用sub指令每次给rax寄存器里的循环计数减一,如果减到0,会修改状态寄存器的值,jne指令不会跳转,循环结束。sub指令和jne指令在SNB架构下和可以做到宏融合,形成单一的微指令,不会产生延迟,在port5和前面的乘法加法同周期分派。
ROME
使用ROME机器,架构是Zen2,架构图如下:
Zen2架构有两个FMA port,FP32理论峰值是2port * (256/32) * 2(mul + add) * 频率 * 核心数,单核,超频3.4GHZ,所以单核峰值应该是108.8GFLOPS。
cpufp 对应f32 FMA的kernel代码应该:
cpufp_kernel_x86_fma_fp32:
mov $0x40000000, %rax
vxorps %ymm0, %ymm0, %ymm0
.cpufp.x86.fma.fp32.L1:
vfmadd231ps %ymm0, %ymm0, %ymm0
sub $0x1, %rax
jne .cpufp.x86.fma.fp32.L1
ret
OPS= 0x40000000L * (256/32) * 2(mul +add) = 0x40000000L * 16,算出循环0x40000000L的总时间time后,得到GFLOPS=OPS / time * 1e-9。
测试得到在ROME上FP32的理论性能:
实际上测的性能只有10.8GFLOPS和108GFLOPS差别较大。是什么原因呢?
这是因为FMA的吞吐和延迟的关系,ROME中一条FMA指令延迟是5 cycles,因为2 ports,所以每个cycles可以吞吐2条FMA指令。这样为了掩盖所有FMA执行周期,至少10条无数据依赖关系的FMA指令才能填满各级流水线。
纵轴代表ROME架构pipe0和pipe1上各有一个FMA单元,每个FMA单元有五级流水线(S0→S4)。
横轴代表时间周期:
第一个周期,最开始的两条fma指令(红色)同时分别发射到port0和port1,并执行流水线的第一阶段(S0);随着时钟的tick,这两条指令分别进入之后的流水阶段(S2到S4);
第二个周期,再发射两条FMA指令(蓝色)到S0;以此类推,到第五个周期,最后两条指令开始发射(灰色),同时最开始的两条指令执行最后一个阶段(S4);
第六个周期开始,循环也回到开头,继续发射两条红色的FMA指令,完美衔接,没有任何气泡。可以想象,只要程序足够长,两个FMA单元的10个流水线阶段(相当于计算资源)绝大部分时间都是有指令在执行的,除了开头进入流水线,和结尾结束流水线。
所以只循环一次的效率比较低,增加了跳转和依赖,流水线平行差,经过上面的方向,10条FMA指令并行度最好,修改后的代码如下:
cpufp_kernel_x86_fma_fp32:
mov $0x40000000, %rax
vxorps %ymm0, %ymm0, %ymm0
.cpufp.x86.fma.fp32.L1:
vfmadd231ps %ymm0, %ymm0, %ymm0
vfmadd231ps %ymm1, %ymm1, %ymm1
vfmadd231ps %ymm2, %ymm2, %ymm2
vfmadd231ps %ymm3, %ymm3, %ymm3
vfmadd231ps %ymm4, %ymm4, %ymm4
vfmadd231ps %ymm5, %ymm5, %ymm5
vfmadd231ps %ymm6, %ymm6, %ymm6
vfmadd231ps %ymm7, %ymm7, %ymm7
vfmadd231ps %ymm8, %ymm8, %ymm8
vfmadd231ps %ymm9, %ymm9, %ymm9
sub $0x1, %rax
jne .cpufp.x86.fma.fp32.L1
ret
OPS= 0x40000000L * (256/32) * 2(mul +add) * 10= 0x40000000L * 160,算出循环0x40000000L的总时间time后,得到GFLOPS=OPS / time * 1e-9。
测试得到在ROME上FP32的理论性能:
108.4524GFLOPS和理论性能108.8GFLOPS非常相近,因为频率不可能达到3.4GFLOPS,所以有点误差是没关系的。
测试发现,大于等于10条FMA指令GFLOPS最好,其他情况均达不到最大峰值。
Hygon Dhyana
国产芯片Hygon Dhyana使用是初代芯片,使用的是阉割的Zen架构,浮点只有一个Port0,且SIMD是128位,超频3GHZ。
FP32理论峰值是1port * (128/32) * 2(mul + add) * 频率 * 核心数,单核超频3GHZ,所以单核峰值应该是24GFLOPS。
因为FMA只有1个port,所以按照上面的分析,5条FMA指令并行度最好。
cpufp_kernel_x86_fma_fp32:
mov $0x40000000, %rax
vxorps %ymm0, %ymm0, %ymm0
.cpufp.x86.fma.fp32.L1:
vfmadd231ps %ymm0, %ymm0, %ymm0
vfmadd231ps %ymm1, %ymm1, %ymm1
vfmadd231ps %ymm2, %ymm2, %ymm2
vfmadd231ps %ymm3, %ymm3, %ymm3
vfmadd231ps %ymm4, %ymm4, %ymm4
sub $0x1, %rax
jne .cpufp.x86.fma.fp32.L1
ret
测试得到在Dhyana上FP32的实际性能,和理论性能基本一致:
验证一下乘加的峰值性能:乘加指令是一条乘法VMULPS和一条加法指令VADDPS的结合
在Dhyana上,VMULPS占用Port0,而VADDPS占用Port2。所以一个cycle可用执行两条指令,如下图:红色、蓝色、青色为VMULPS指令,粉色、天蓝色、绿色为VADDPS指令,在一个cycle中,可以同时存在VMULPS和VADDPS指令。
FP32理论峰值是1port * (128/32) * 2(mul + add) * 频率 * 核心数,单核超频3GHZ,所以单核峰值应该是24GFLOPS。
因为只有1个port,所以按照上面的分析,3条VMULPS和3条VADDPS指令并行度最好。
cpufp_kernel_x86_fma_fp32:
mov $0x40000000, %rax
vxorps %ymm0, %ymm0, %ymm0
.cpufp.x86.fma.fp32.L1:
mulps %xmm0, %xmm0
addps %xmm1, %xmm1
mulps %xmm2, %xmm2
addps %xmm3, %xmm3
mulps %xmm4, %xmm4
addps %xmm5, %xmm5
sub $0x1, %rax
jne .cpufp.x86.fma.fp32.L1
ret
测试得到在Dhyana上FP32的实际性能,和理论性能基本一致:
INT8性能峰值分析
int8使用vpmaddubsw、vpmaddwd、vpaddd三条指令合成,如下图:
我们首先一条一条指令分析。(以Hygon Dhyana为例,1port,超频3GHZ,YMM=XMM+XMM拼接)
乘加指令操作分析:以XMM为例,ops = (128/16)*(2mul + 1add) = 24, fpc = ops / 1cycle = 24。单核理论峰值为:24 * 3GHZ = 72GFLOPS
Latency = 4cycle,1port。上面分析可以知道,四条VPMADDUBSW指令可以满足最大流水,
测试代码如下:
.globl cpufp_kernel_x86_sse_int8
cpufp_kernel_x86_sse_int8:
mov $400000000, %rax
.cpufp.x86.avx.int8.L1:
vpmaddubsw %xmm0, %xmm0, %xmm0
vpmaddubsw %xmm3, %xmm3, %xmm3
vpmaddubsw %xmm6, %xmm6, %xmm6
vpmaddubsw %xmm9, %xmm9, %xmm9
sub $0x1, %rax
jne .cpufp.x86.avx.int8.L1
ret
测试得到在Dhyana上VPMADDUBSW的实际性能,和理论性能基本一致:
VPMADDWD
乘加指令操作分析:以XMM为例,ops = (128/32*(2mul + 1add) = 12, fpc = ops / 1cycle = 12。单核理论峰值为:12 * 3GHZ = 36GFLOPS
Latency = 3cycle,1port。上面分析可以知道,三条VPMADDWD指令可以满足最大流水,
测试代码如下:
.globl cpufp_kernel_x86_sse_int8
cpufp_kernel_x86_sse_int8:
mov $400000000, %rax
.cpufp.x86.avx.int8.L1:
vpmaddwd %xmm1, %xmm1, %xmm1
vpmaddwd %xmm4, %xmm4, %xmm4
vpmaddwd %xmm7, %xmm7, %xmm7
sub $0x1, %rax
jne .cpufp.x86.avx.int8.L1
ret
测试得到在Dhyana上VPMADDWD的实际性能,和理论性能基本一致:
VPADDDD
乘加指令操作分析:以XMM为例,ops = (128/32*(1add) = 4 , fpc = ops / 1cycle = 4。单核理论峰值为:cores * core freequency * fpc = 2port * 4 * 3GHZ = 24GFLOPS
Latency = 1cycle,2port都可用。上面分析可以知道,两条VPADDDD指令可以满足最大流水,测试代码如下:
.globl cpufp_kernel_x86_sse_int8
cpufp_kernel_x86_sse_int8:
mov $400000000, %rax
.cpufp.x86.avx.int8.L1:
vpaddd %xmm2, %xmm2, %xmm2
vpaddd %xmm5, %xmm5, %xmm5
sub $0x1, %rax
jne .cpufp.x86.avx.int8.L1
ret
测试得到在Dhyana上VPADDDD的实际性能,和理论性能基本一致:
VPMADDUBSW + VPMADDWD
乘加指令操作分析:以XMM为例,
VPMADDUBSW:ops = (128/16)*(2mul + 1add) = 24
VPMADDWD:ops = (128/32*(2mul + 1add) = 12
总ops = 36,传送两条指令,需要2个时钟周期,所以fpc = ops / cycle = 36 / 2 = 18, 单核理论峰值为:FLOPS = cores * cpu freequency per core * fpc = 1 * 3 * 18 = 54GFLOPS。
使用cpufp测试代码:
.globl cpufp_kernel_x86_sse_int8
cpufp_kernel_x86_sse_int8:
mov $400000000, %rax
.cpufp.x86.avx.int8.L1:
vpmaddubsw %xmm0, %xmm0, %xmm0
vpmaddwd %xmm1, %xmm1, %xmm1
vpmaddubsw %xmm3, %xmm3, %xmm3
vpmaddwd %xmm4, %xmm4, %xmm4
vpmaddubsw %xmm6, %xmm6, %xmm6
vpmaddwd %xmm7, %xmm7, %xmm7
vpmaddubsw %xmm9, %xmm9, %xmm9
vpmaddwd %xmm10, %xmm10, %xmm10
vpmaddubsw %xmm12, %xmm12, %xmm12
vpmaddwd %xmm13, %xmm13, %xmm13
sub $0x1, %rax
jne .cpufp.x86.avx.int8.L1
ret
循环次数为400000000,即n=400000000*5
测试后性能为:考虑到其他原因,基本与理论性能一致。
VPMADDUBSW + VPMADDWD + VPADDD
乘加指令操作分析:以XMM为例,
VPMADDUBSW:ops = (128/16)*(2mul + 1add) = 24
VPMADDWD:ops = (128/32*(2mul + 1add) = 12
VPADDD:ops = (128/32*1add) = 4
总ops = 40,因为VPADDD可用两个port,所以VPMADDUBSW和VPMADDWD执行时,对VPADDD没有影响,可用并行计算,且latency = 1。所以三条指令理论只需要2个时钟周期,所以fpc = ops / cycle = 40 / 2 = 20, 单核理论峰值为:FLOPS = cores * cpu freequency per core * fpc = 1 * 3 * 20 = 60GFLOPS
流水线如上图:红色为VPMADDUBSW,蓝色为VPADDD,紫色为VPADDD。因为VPADDD cycle为1,第一阶段变执行结束。所以不影响VPMADDUBSW和VPMADDWD的执行。
使用cpufp测试代码:
.globl cpufp_kernel_x86_sse_int8
cpufp_kernel_x86_sse_int8:
mov $400000000, %rax
.cpufp.x86.avx.int8.L1:
vpmaddubsw %xmm0, %xmm0, %xmm0
vpmaddwd %xmm1, %xmm1, %xmm1
vpaddd %xmm2, %xmm2, %xmm2
vpmaddubsw %xmm3, %xmm3, %xmm3
vpmaddwd %xmm4, %xmm4, %xmm4
vpaddd %xmm5, %xmm5, %xmm5
vpmaddubsw %xmm6, %xmm6, %xmm6
vpmaddwd %xmm7, %xmm7, %xmm7
vpaddd %xmm8, %xmm8, %xmm8
vpmaddubsw %xmm9, %xmm9, %xmm9
vpmaddwd %xmm10, %xmm10, %xmm10
vpaddd %xmm11, %xmm11, %xmm11
vpmaddubsw %xmm12, %xmm12, %xmm12
vpmaddwd %xmm13, %xmm13, %xmm13
vpaddd %xmm14, %xmm14, %xmm14
sub $0x1, %rax
jne .cpufp.x86.avx.int8.L1
ret
测试后性能为:考虑到其他原因,基本与理论性能一致。
更进一步分析int8
再进一步分析INT8,发现VPMADDWD的SRC2为1,并没有有效数据,所以ops计算应该修改如下:
乘加指令操作分析:以XMM为例,
VPMADDUBSW:ops = (128/16)*(2mul + 1add) = 24
VPMADDWD:ops = (128/32*1add) = 4 (乘法无效,因为src2没有数据)
VPADDD:ops = (128/32*1add) = 4
总ops = 32,所以fpc = ops / cycle = 32 / 2 = 16, 单核理论峰值为:FLOPS = cores * cpu freequency per core * fpc = 1 * 3 * 16 = 48GFLOPS
测试后性能为:考虑到其他原因,基本与理论性能一致。
寄存器分块
上面说明了如何最大利用流水线,在实际应用中如何充分使用逻辑寄存器,并达到最大利用流水线的效果呢?
FP32
以FP32为例,使用FMA指令,在2 Port的情况下,至少要10条FMA指令,才能达到流水最大效率。
所以在GEMM操作中,分块的思想有如下几种:
4x3寄存器分块
此时,最内层循环一共有12个FMA,大于10个,所以流水线被填满,利用率最高,此时寄存器总量为:4 x 3 + 3 + 1 = 16个。
当然,5x2和2x5都没有问题,这种情况下,只需要2 x 5 + 2 + 1 = 13个逻辑寄存器就可以打满峰值。
下面出现一个问题,既然要保证10个以上的寄存器就可以满足流水,那能不能用10x1的分块呢?如下图:
10x1寄存器分块
看似比较合理,共使用10x1+1+1 = 12个寄存器,有10FMA执行,pipeline是没有任何问题。
通过上面Zen的架构图可以看到,LD有两个port,假如所有的数据都在L1 cache里,LD指令就可以流水执行,大约4~5个cycles可以把一条向量从L1 cache读到寄存器中。这样,每个时钟周期,Zen CPU就可以同时发射两条FMA和两条VMOVUPS(或者Vbroadcastss)指令。这就要求,总的Load指令数量最多和FMA指令数量相等,一旦超过,FMA将不是瓶颈,LD指令将变成瓶颈。对于10×1的分块,我们需要10+1=11次LD才能喂给10条FMA指令,这样一定不能打满FMA的峰值;对于4×3的分块,我们需要4+3=7次LD就能喂给12条FMA指令。
所以,这个结论可以推广为:LD和FMA指令的比例,要小于等于处理器提供的LD和FMA单元同周期发射能力的比例,如果是某些顺序单发射或者乱序发射能力有限的架构,甚至要把这个比例在寄存器数量能支撑的范围内降到最低,这样可以保证更多周期是在发射FMA。其实对于m x n的寄存器分块,LD的数量一般是m + n,所以尽量构造一个足够大的接近方形的分块,及m x n / (m + n) 最大的情况下,可以最大地利用FMA的峰值性能。
INT8
INT8的情况和FP32还是不同。VPMADDUBSW、VPMADDWD、VPMADDD三条指令中前两条共用一个Pipeline,且VPMADDUBSW的latency是4cycles,VPMADDWD的latency是3cycles。所以大于等于4次循环,即可以达到流水线最大利用率。同时结合上面的分析,LD和FMA综合考虑,可以分为下面几种寄存器分块情况:6x2、4x3、4x2三种情况。
从矩阵A中广播四个INT8的指令目前没有,可以使用指令合成为:uni_vpbroadcastd,即通过两条指令合成。
void uni_vpbroadcastd(const Xbyak::Xmm &x, const Xbyak::Operand &op) {
vmovss(x, op);
vpshufd(x, x, 0x0);
}
其中VMOVSS的latency为4cycles,使用LD port;VPSHUFD的latency为1cycle,使用两个pipeline(FP1/2)。和VPMADDUBSW、VPMADDWD使用不是同一个pipeline(FP0),不存在竞争关系。
6x2分块
4x3分块
4x2分块
上面的三种分块,流水线均可以满足最优情况,所以只需要考虑LD的效率,尽量让LD的lantecy最低,即( m x n )/ (m + n)最大。
( 6 x 2 ) / ( 6 + 2) = 1.5
( 4 x 3 ) / ( 4 + 3) = 1.7
( 4 x 2 ) / ( 4 + 2) = 1.3
从上面的比率来看,4x3的寄存器分块似乎最好,但是要考虑到M和N的平衡性,以及边界处理,寄存器分块可以从4x3和6x2中选择。
1:M和N的平衡性
若4x3分块,A的M=4,B的N=3x4=12,M和N差别较大,比较适合N较大的情况,即较窄的矩阵;若6x2分块,A的M=6,B的N=2x4=8,M和N差别较小,适合大部分矩阵。
2:边界处理
若4x3分块,B的N=3的维度长度为4x3=12,则小于12的都会损失性能,而6x2分块,B的N=2的维度长度为4x2=8,小于8性能会损失,8相对12边界处理更好一些。
3:逻辑寄存器数量
AVX逻辑寄存器只有16个,若使用4x3分块,则使用4 x 3 + 1 + 3 + 1 + 1 > 16;若使用6x2分块,则使用6 x 2 + 1 + 2 + 1 + 1 > 16;若使用4x2分块,则使用4 x 2 + 1 + 2 + 1 + 1 = 13 < 16.(VPMADDUBSW额外需要一个XMM/YMM寄存器,VPMADDWD额外需要一个XMM/YMM寄存器)
所以只有4x2分块才能满足逻辑寄存器数量。
目前优化代码使用4x2的寄存器分块。
说明
本文章在参考高叔叔 高洋:浮点峰值那些事儿 的基础上,拓展了INT8的峰值计算。
编辑于 2022-09-15 14:24