性能峰值那点事--CPUFP分析

CPUFP分析 - 知乎 (zhihu.com)

工欲善其事,必先利其器,在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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值