理解计算平台的两个指标
算力 π \pi π:计算平台的性能上限,指的是一个计算平台倾尽全力每秒钟所能完成的浮点运算数。单位是 FLOPS or FLOP/s。
带宽 β \beta β:也即计算平台的带宽上限,指的是一个计算平台倾尽全力每秒所能完成的内存交换量。单位是Byte/s。
计算强度上限
I
I
I:两个指标相除即可得到计算平台的计算强度上限。它描述的是在这个计算平台上,单位内存交换最多用来进行多少次计算。单位是FLOPs/Byte。
I
=
π
/
β
I = \pi / \beta
I=π/β
这里所说的“内存”是广义上的内存。对于CPU计算平台而言指的就是真正的内存;而对于GPU计算平台指的则是显存。
神经网络模型的两个指标
-
计算量:指的是输入单个样本(对于CNN而言就是一张图像),模型进行一次完整的前向传播所发生的浮点运算个数,也即模型的时间复杂度。单位是 FLOP or FLOPs。其中卷积层的计算量公式如下
M 2 ˆ × K 2 × C i n × C o u t M\^{2} \times K^{2} \times C_{in} \times C_{out} M2ˆ×K2×Cin×Cout -
访存量:指的是输入单个样本,模型完成一次前向传播过程中所发生的内存交换总量,也即模型的空间复杂度。在理想情况下(即不考虑片上缓存),模型的访存量就是模型各层权重参数的内存占用(Kernel Mem)与每层所输出的特征图的内存占用(Output Mem)之和。单位是Byte。由于数据类型通常为float32(共占用了4字节) ,因此需要乘以四。
KaTeX parse error: Undefined control sequence: \tiems at position 63: …imes C_{out} ) \̲t̲i̲e̲m̲s̲ ̲4 -
模型的计算强度:由计算量除以访存量就可以得到模型的计算强度,它表示此模型在计算过程中,每Byte内存交换到底用于进行多少次浮点运算。单位是FLOPs/Byte。可以看到,模计算强度越大,其内存使用效率越高。
roof-line model
其实 Roof-line Model 说的是很简单的一件事:模型在一个计算平台的限制下,到底能达到多快的浮点计算速度。更具体的说,Roof-line Model 意义是 : 计算量为A且访存量为B的模型在算力为C且带宽为D的计算平台所能达到的理论性能上限E是多少。
Roof-line 的形态
所谓“Roof-line”,指的就是由计算平台的算力和带宽上限这两个参数所决定的“屋顶”形态,如下图所示。
算力决定“屋顶”的高度(绿色线段)
带宽决定“房檐”的斜率(红色线段)
Roof-line 划分出的两个瓶颈区域
计算瓶颈区域 Compute-Bound
不管模型的计算强度有多大,它的理论性能最大只能等于计算平台的算力 。当模型的计算强度大于计算平台的计算强度上限时,模型在当前计算平台处于 Compute-Bound 状态,即模型的理论性能受到计算平台算力的限制,无法与计算强度成正比。但这其实并不是一件坏事,因为从充分利用计算平台算力的角度上看,此时模型已经的利用了计算平台的全部算力。可见,计算平台的算力越高,模型进入计算瓶颈区域后的理论性能也就越大。
带宽瓶颈区域 Memory-Bound
当模型的计算强度小于计算平台的计算强度上限时,由于此时模型位于“房檐”区间,因此模型理论性能的大小完全由计算平台的带宽上限(房檐的斜率)以及模型自身的计算强度所决定,因此这时候就称模型处于 Memory-Bound 状态。可见,在模型处于带宽瓶颈区间的前提下,计算平台的带宽越大(房檐越陡),或者模型的计算强度越大,模型的理论性能可呈线性增长。
这一部分摘自知乎博主,Roofline Model与深度学习模型的性能分析
注意这副图和之前的差别
如下图所示,斜线区域表示的带宽瓶颈,水平线表示的是计算瓶颈。不同的机制会导致计算峰值下降(如SIMD等),该变化反映到水平线的下降;不同的硬件会导致带宽上线的下降(如软件预取、NUMA、L1 Cache),该变化会反应的截距上,即roolline模型的起点可能在原点上方或右方。
也很好理解,因为 I I I是峰值算力除以峰值带宽,即可也认为处理器的算力强度是不变的, π = β × I \pi=\beta \times I π=β×I,当使用峰值带宽的时候,算力自然大,但是带宽下降时,算力自然就减小,因此该变化反应到了截距上。
- 不使用预先抓取数据,忽略非统一内存访问(NUMA)等将会降低可实现带宽;不同级别的内存有着不同的带宽(HBM 与DRAM)
- 水平线的下移依次表示:只计算加法,没有乘法–>不使用SIMD->不使用ILP导致所实现的峰值计算率不断衰减;
- 斜线的下移依次表示:不使用软件预抓取数据→在分布式系统中不使用NUMA使得所实现的带宽降低。
其实做成平行线没什么道理,可以理解成为了方便看图这样做的吧
mperf
介绍
git项目
教程
mperf绘制roof-line模型
mperf 使用
#include "opencv2/core.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/imgproc.hpp"
#include "mperf/cpu_affinity.h"
#include "mperf/timer.h"
#include "mperf/tma/tma.h"
int main(){
cv::Mat image = cv::imread("test.png");
cv::Mat image1;
size_t iter_num = 100;
mperf::tma::MPFTMA mpf_tma(mperf::MPFXPUType::A55);
mpf_tma.init({"Frontend_Bound",
"Fetch_Latency",
"ICache_Misses",
"ITLB_Misses",
"Predecode_Error",
"Fetch_Bandwidth",
"Bad_Speculation",
"Branch_Mispredicts",
"Backend_Bound",
"Memory_Bound",
"Load_Bound",
"Load_DTLB",
"Load_Cache",
"Store_Bound",
"Store_TLB",
"Store_Buffer",
"Core_Bound",
"Interlock_Bound",
"Interlock_AGU",
"Interlock_FPU",
"Core_Bound_Others",
"Retiring",
"LD_Retiring",
"ST_Retiring",
"DP_Retiring",
"ASE_Retiring",
"VFP_Retiring",
"PC_Write_Retiring",
"BR_IMMED_Retiring",
"BR_RETURN_Retiring",
"BR_INDIRECT_Retiring",
"Metric_L1D_Miss_Ratio",
"Metric_L1D_RD_Miss_Ratio",
"Metric_L1D_WR_Miss_Ratio",
"Metric_L2D_Miss_Ratio",
"Metric_L2D_RD_Miss_Ratio",
"Metric_L2D_WR_Miss_Ratio",
"Metric_L3D_Miss_Ratio",
"Metric_L3D_RD_Miss_Ratio",
"Metric_BR_Mispred_Ratio",
"Metric_L1I_TLB_Miss_Ratio",
"Metric_L1D_TLB_Miss_Ratio",
"Metric_L2_TLB_Miss_Ratio",
"Metric_ITLB_Table_Walk_Ratio",
"Metric_DTLB_Table_Walk_Ratio",
"Metric_Load_Port_Util",
"Metric_Store_Port_Util",
"Metric_FPU_Util",
"Metric_GFLOPs_Use",
"Metric_L3_BW_Use",
"Metric_L2_BW_Use",
"Metric_DRAM_BW_Use"
});
size_t gn = mpf_tma.group_num();
size_t uncore_evt_num = mpf_tma.uncore_events_num();
printf("the gn and uncore_evt_nums %zu, %zu\n", gn, uncore_evt_num);
for (size_t i = 0; i < gn; ++i) {
mpf_tma.start(i);
for(size_t j = 0; j < iter_num; ++j){
cv::GaussianBlur(image, image1, cv::Size(3, 3), 0.8);
}
mpf_tma.sample_and_stop(iter_num);
}
for (size_t i = 0; i < uncore_evt_num; ++i) {
mpf_tma.start_uncore(i);
for(size_t j = 0; j < iter_num; ++j){
cv::GaussianBlur(image, image1, cv::Size(3, 3), 0.8);
mpf_tma.sample(1);
}
mpf_tma.sample_and_stop(iter_num);
}
mpf_tma.deinit();
return 0;
}
mperf 优化教程
矩阵乘法的Naive实现为三层循环计算,两个矩阵都是900 * 900:
测试平台:phantom X2 pro 5G
环境:0号核心。最高频
naive
//lda:number of columns of A
//ldb:number of columns of B
//ldc:number of columns of C
#define A(i, j) a[(i)*lda + (j)]
#define B(i, j) b[(i)*ldb + (j)]
#define C(i, j) c[(i)*ldc + (j)]
void my_matmul_naive(int m, int n, int k, float* a, int lda, float* b, int ldb,
float* c, int ldc) {
int i, j, p;
for (i = 0; i < m; i++) {
for (j = 0; j < n; j++) {
for (p = 0; p < k; p++) {
C(i, j) = C(i, j) + A(i, p) * B(p, j);
}
}
}
}
通过mperf测试的指标,分析可知,该段代码的GFPLOS只有0.4,而ARM A55的峰值计算量为14GFLOPs,有很大的优化空间。
观察到FPU_Util为0,因为此时没有进行向量化计算,说明了 Naive 实现未能充分利用处理器的SIMD单元。解决方法是循环展开。
unroll
void my_matmul_unroll(int m, int n, int k, float* a, int lda, float* b, int ldb,
float* c, int ldc) {
int i, j;
for (j = 0; j < n;
j += 12) {
if (j + 12 > n)
break;
for (i = 0; i < m; i += 8) {
if (i + 8 > m)
break;
AddDot8x12(k, &A(i, 0), lda, &B(0, j), ldb, &C(i, j), ldc);
}
if (i != m) {
AddDot4x12(k, &A(i, 0), lda, &B(0, j), ldb, &C(i, j), ldc);
}
}
if (j != n) {
for (; j < n; j += 4) {
for (i = 0; i < m; i += 8) {
if (i + 8 > m)
break;
AddDot8x4(k, &A(i, 0), lda, &B(0, j), ldb, &C(i, j), ldc);
}
if (i != m) {
AddDot4x4(k, &A(i, 0), lda, &B(0, j), ldb, &C(i, j), ldc);
}
}
}
}
分析mperf数据可知:FPU_util明显上升,GFLOPS有数量级的上升。
但是memory_bound很大,而Memory_Bound占比高主要是 Load_cache 造成的,接下来要进行访存优化。解决方法包括:分块和pack。
block
#define kc 256
#define nc 252
void my_mamtmul_block(int m, int n, int k, float *a, int lda, float *b, int ldb,
float *c, int ldc) {
int j, p, pb, ib;
for (p = 0; p < k; p += kc) {
pb = min(k - p, kc);
for (j = 0; j < n; j += nc) {
ib = min(n - j, nc);
InnerKernel(m, ib, pb, &A(0, p), lda, &B(p, j), ldb, &C(0, j), ldc);
}
}
}
观察mperf测试数据可知,GFLOPS确实上升不少,FPU利用率也得到了提高,Load_Cache 也有所下降。说明分块确实减少了对 latency 非常大的系统主存的访问,接下来使用pack方法优化
pack
arm cpu引入了SIMD指令,支持VFP(向量浮点运算),通过 pack 成向量,使用 SIMD 单元
过程:
void InnerKernel(int m, int n, int k, float* a, int lda, float* b, int ldb,
float* c, int ldc) {
int i, j;
float packedA[m * k];
float packedB[k * n];
for (j = 0; j < n; j += 12) {
if (j + 12 > n)
break;
PackMatrixB_12(k, &B(0, j), ldb, packedB + j * k);
for (i = 0; i < m; i += 8) {
if (i + 8 > m)
break;
if (0 == j) {
PackMatrixA_8(k, &A(i, 0), lda, packedA + i * k);
}
AddDot8x12(k, packedA + i * k, k, packedB + j * k, 12, &C(i, j),
ldc);
}
if (i != m) {
PackMatrixA_4(k, &A(i, 0), lda, packedA + i * k);
AddDot4x12(k, packedA + i * k, k, packedB + j * k, 12, &C(i, j),
ldc);
}
}
if (j != n) {
for (; j < n; j += 4) {
PackMatrixB_4(k, &B(0, j), ldb, packedB + j * k);
for (i = 0; i < m; i += 8) {
if (i + 8 > m)
break;
AddDot8x4(k, packedA + i * k, k, packedB + j * k, 12, &C(i, j),
ldc);
}
if (i != m) {
AddDot4x4(k, packedA + i * k, k, packedB + j * k, 12, &C(i, j),
ldc);
}
}
}
}
优化后GFLOPS又有一定幅度的增长,但非常小,说明虚拟内存系统缺页处理非常完善,虚拟分页系统存在页面预取机制。Load_Cache明显下降。
但是观察到 core_bound 占比非常大,说明寄存器单元的使用率不高,通过把内层计算逻辑替换为嵌入式汇编,依据架构硬件特性调整指令选择和指令排布,进一步减少pipeline上的依赖和冲突。
pipeline优化-嵌入汇编
ldq跟fmla不能双发射(注:ARM A55是双发射架构),这就验证了ldq会造成计算和访存指令无法双发射,并导致了PU_util数值的下降。进一步我们发现,ldr,ldr,ins三种指令都可以与fmla双发射,且发射都是1周期,而这三条指令可以组合出ldq等价的功能。因此可以想象,使用ldr,ldr,ins指令组合替换ldq指令,可以提高流水线的满载程度,进而提高性能。
asm volatile(
"fmla v1.4s, v1.4s, v1.s[0]\n"
"ldr d0, [%[b_ptr]]\n"
"fmla v2.4s, v2.4s, v2.s[0]\n"
"ldr x0, [%[b_ptr], #8]\n"
"fmla v3.4s, v3.4s, v3.s[0]\n"
"ins v0.d[1], x0\n"
"fmla v4.4s, v4.4s, v4.s[0]\n"
"ldr d7, [%[b_ptr]]\n"
"fmla v5.4s, v5.4s, v5.s[0]\n"
"ldr x0, [%[b_ptr], #8]\n"
"fmla v6.4s, v6.4s, v6.s[0]\n"
"ins v7.d[1], x0\n"
: [b_ptr] "+r"(b_ptr)
:
: "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7");
可以看到 core bound 有所降低,GFLOPs大幅提升。
mperf 优化教程