Open zhaoyuchen2018 opened 5 years ago
@Aurelius84 提供的profile对比
sse_axpy
和sse_axpy_noadd
,可以尝试调用MKL。@luotao1使用gperftools分析得到:bloomfilter_get
在前向中占比最多。15%里面占比13%。
确定path
lookup_table
:走的是LoDTensor path,path里面的padding_idx走了少数几次lookup_table_grad
:走的是sparse的非grad_inplace pathsequence_pool
:走的是SUM path结论:参照竞品的做法
具体分析:
lookup_table
) 和 sequence_pool
(sum)这两个OP都非计算密集的算子,主要时间耗费在内存搬移上。
lookup_table
OP: 根据word id查表,把每个词向量从字典中逐个拷贝到output里 (就是memcpy)sequence_pool
OP (sum): 将输入(即lookup_table
的output)的所有词向量,做向量加后,写入output竞品分析:
lookup_table
+ seq_pool
(sum)时,无需先将词向量从字典中拷贝出来,而是直接将字典中特定的行(词向量)直接加起来。总结:单独对这两个OP优化,是无法起到上面这个“减少内存访问”的效果的。
lookup_table
和sequence_pool
前向Op,在MKLML测试版本基础上,能够编译通过且通过unit test,具体见CODE @intel, 2019年7月25日从timeline上来看,search_pyramid_hash时间都消耗在CPU上
从Vtune结果看,memcpy
占比14%
cmd: perf stat sh run_train.sh
system | task-clock | context-switches | cpu-migrations | page-faults | cycles | instructions | branches | branch-misses |
---|---|---|---|---|---|---|---|---|
paddle | 1.252 CPUs utilized | 770,000(0.002 M/sec) | 16,462(0.042 K/sec) | 10,591,434(0.027 M/sec) | 2.288 GHz | 1.35 insns per cycle | 429.774 M/sec | 21.35% of all branches |
竞品 | 1.001 CPUs utilized | 3,979(0.000 M/sec) | 8,385(0.000 M/sec) | 5,017,948(0.010 M/sec) | 2.284 GHz | 1.50 insns per cycle | 296.205 M/sec | 25.53% of all branches |
上面是perf的结果,可以看出来paddle相比竞品 context switch多了~200倍,会影响CPU的执行效率。
bloomfilter_get
内部占比:
sse_axpy
走到sse
指令,而非avx
指令。但这部分占比很小,改后https://github.com/PaddlePaddle/Paddle/pull/18814/commits/64c7fd92c1bbda62ddce4c6ec9a102321bfc482c 基本没变化。
murmurhash3_x64_128优化
优化前:
switch (len & 15) {
case 15:
nk2 ^= ((uint64_t) tail[14]) << 48;
case 14:
nk2 ^= ((uint64_t) tail[13]) << 40;
case 13:
nk2 ^= ((uint64_t) tail[12]) << 32;
case 12:
nk2 ^= ((uint64_t) tail[11]) << 24;
case 11:
nk2 ^= ((uint64_t) tail[10]) << 16;
case 10:
nk2 ^= ((uint64_t) tail[9]) << 8;
case 9:
nk2 ^= ((uint64_t) tail[8]) << 0;
nk2 *= c2;
nk2 = ROTL64(nk2, 33);
nk2 *= c1;
h2 ^= nk2;
case 8:
nk1 ^= ((uint64_t) tail[7]) << 56;
case 7:
nk1 ^= ((uint64_t) tail[6]) << 48;
case 6:
nk1 ^= ((uint64_t) tail[5]) << 40;
case 5:
nk1 ^= ((uint64_t) tail[4]) << 32;
case 4:
nk1 ^= ((uint64_t) tail[3]) << 24;
case 3:
nk1 ^= ((uint64_t) tail[2]) << 16;
case 2:
nk1 ^= ((uint64_t) tail[1]) << 8;
case 1:
nk1 ^= ((uint64_t) tail[0]) << 0;
nk1 *= c1;
nk1 = ROTL64(nk1, 31);
nk1 *= c2;
h1 ^= nk1;
};
优化后:
uint64_t tail0_64 = *(uint64_t*)(tail);
uint64_t tail_64 = *(uint64_t*)(tail + 8);
uint64_t mask0 = 0xffffffffffffffff;
uint64_t mask = 0x00ffffffffffffff;
int flag = len & 15;
if (flag && flag <= 8) {
tail0_64 &= (mask0 >> ((8 - flag)<<3));
} else if (flag > 8){
tail_64 &= (mask >> ((15 - flag)<<3));
nk2 ^= tail_64;
nk2 *= c2;
nk2 = ROTL64(nk2, 33);
nk2 *= c1;
h2 ^= nk2;
}
if (flag) {
nk1 ^= tail0_64;
nk1 *= c1;
nk1 = ROTL64(nk1, 31);
nk1 *= c2;
h1 ^= nk1;
}
op 时间:
测试次数 | 1 | 2 | 3 | avg |
---|---|---|---|---|
优化前(ms) | 3.43268 | 3.27932 | 3.25202 | 3.32 |
优化后(ms) | 3.33146 | 3.32852 | 3.13379 | 3.26 |
@luotao1 关于perf log里观察到context_switch的次数比竞品高200倍的问题,我看了一下vtune log。
一个疑问是:是否这些“无用的”OpenBLAS线程导致了context_switch次数的增加? 尽管它们不占用多少CPU时间,但会增加CPU调度的几率。
@jianhang-liu 初步怀疑是numpy调用的OpenBlas库,类似issue:https://github.com/ray-project/ray/issues/1654 https://github.com/tensorflow/tensorflow/issues/17315
可以试试用export OPENBLAS_NUM_THREADS=1(或通过代码在runtime进行设置)控制OpenBLAS线程数,然后用Vtune看看是否生效
从下面两点做了尝试:
%
操作放到bit_get
之后,这样如果第一个bit_get
返回之后有一定的概率省去第二次的%
操作。考虑到64bit的div
操作比较耗时,在做之前进行判断,看是否需要64bit的%
操作。
if (result[0] < (uint32_t)0xffffffff) {
result[0] = (uint32_t)result[0] % (uint32_t)bloomfilter->m;
} else {
result[0] %= bloomfilter->m;
}
if (!bit_get(bloomfilter->bit_vector, result[0])){
return 0;
}
if (result[1] < (uint32_t)0xffffffff) {
result[1] = (uint32_t)result[1] % (uint32_t)bloomfilter->m;
} else {
result[1] %= bloomfilter->m;
}
if (!bit_get(bloomfilter->bit_vector, result[1])){
return 0;
}
复现了 @zhaoyuchen2018 在https://github.com/PaddlePaddle/benchmark/issues/151#issuecomment-517095618 中的结论。
system | task-clock | context-switches | cpu-migrations | page-faults | cycles | instructions | branches | branch-misses |
---|---|---|---|---|---|---|---|---|
paddle | 1.259 CPUs utilized | 586,038(0.606 K/sec) | 22,668(0.023 K/sec) | 28,124,157(0.029 M/sec) | 2.587 GHz | 1.49 insns per cycle | 122.262 M/sec | 0.71% of all branches |
竞品 | 1.000 CPUs utilized | 4,969(0.000 M/sec) | 1,851(0.000 M/sec) | 5,959,013(0.010 M/sec) | 2.595 GHz | 1.52 insns per cycle | 36.894 M/sec | 1.15% of all branches |
机器和 @zhaoyuchen2018 的不一样。但占比类似。可以看出来paddle相比竞品
跑3个epoch的对比,hotspots
paddle: 从上图可以看出CPU0+CPU1+CPU2+CPU3=elapsed time(747.4s), 其中CPU1=540.9s,CPU2=190.9s。 竞品: 竞品CPU1=605.4s,elapsed time=(610.3s)几乎所有时间都在CPU1. paddle主要有2个CPU在run,猜测CPU2上的计算增加了整体的耗时。
根据骆涛给出的paddle上的vtune log, 我们可以看到有一个主线程和两个子线程较忙: 在vtune的Bottom-up中我们可以看到,虽然起了非常多的thread,但是真正运行的只有前四个线程。(绿色为没有任务,褐色表示正忙,红色表示在等) 其中PID 8702和PID 8701对应的是我们第一张图中那两个较繁忙的子线程。 我们选择一段三个线程都较为繁忙的时间来查看具体某一个线程做了什么。
线程PID 8701 主要在feed data 对应data_feeder.py这个文件,其中有有些时间在等待,可以从彩色条形图里看到一些红色cpu spin time
线程PID 8702 这个线程主要在read data,对应pyramid_data_reader.py这个文件,同样可以看到有部分等待时间。
主线程 主线程基本在忙,主要在做memcpy,bllomfilter_get和CPUSearchPyramidHash
所以这个是训练的过程,是因为用了parallel executor所以一边在做计算,一遍再feed和read data准备下一个batch么?
是因为用了parallel executor所以一边在做计算,一遍再feed和read data准备下一个batch么?
@bingyanghuang 是的。PE里读数据和主线程计算是异步的。
去掉了read lock重新profile的结果:https://github.com/PaddlePaddle/Paddle/pull/19203 可以看出只有一个主线程core在忙,其他占比时间都很少。 从CPU运行时间上看,也是除了一开始有3个线程在RUN之外,后面只有主线程在run: 从函数消耗时间上看,reader函数消耗的时间都很少:
使用amplxe-cl -collect threading sh run_train.sh
,日志截图如下
其中wait_count=5544,是iteration数。
@jianhang-liu 的分析报告见 PyramidDNN模型分析(training).pdf
慢的25%时间的9.0%: Paddle的CPUSearchPrymidHashOPKernel(前向)实现,比竞品对应的 BatchPyramidHashLayer(前向)实现,要慢60s (两者反向的性能基本一致)。而在60s 里,有36s是因为Paddle里的memcpy比Lego里的__memcpy_sse2_unligned多花的
建议:
Paddle的CPUSearchPrymidHashOP和竞品相比,多了一个补0操作:也就是在一个空输入下,补一行0。这样会导致该Op的输出矩阵是比竞品高的。这样能解释两个现象:
建议:
sequence_pool(input, pool_type, is_test=False, pad_value=0.0)
也有补0的操作,可以把补0从CPUSearchPrymidHashOP挪到sequence_pool,和竞品对齐么?去掉reader lock测试了一个epoch(5500个iter)的整体时间:https://github.com/PaddlePaddle/Paddle/pull/19203
测试次数 | 1 | 2 | 3 | avg |
---|---|---|---|---|
有lock(s) | 244 | 270 | 246 | 253 |
没有lock(s) | 239 | 250 | 250 | 246 |
lock对整体时间大概有2%的影响
根据补0操作的分析 https://github.com/PaddlePaddle/benchmark/issues/151#issuecomment-522403630 , 尝试了@ Aurelius84 https://github.com/PaddlePaddle/Paddle/pull/19290
逻辑是这样的,在pyramid_hash里做padding,之后在squence_pool里是不会再走padding逻辑的,因为不存在有长度为0的sequence; 如果不在pyramid_hash里做padding,则输出分为以下两种情况:
使用AVX汇编的memcpy.S,能看到search_pyramid_hash中的memcpy的时间(35s-》30s)降下来了,但整体时间没有明显变化。
memcpy(top_pos + j, weights + pos, _rand_len * sizeof(T));
其中_rand_len * sizeof(T)
=16*sizeof(float)
测试多线程的结果: paddle设置:thread_num = 8, reader_threads = 8 竞品设置:thread_num = 8
测试次数 | 1 | 2 | 3 | avg |
---|---|---|---|---|
paddle(s) | 365 | 372 | 322 | 353 |
竞品(s) | 252 | 254 | 228 | 245 |
paddle比竞品慢44%
@jianhang-liu @bingyanghuang @yinghu5
@Aurelius84 在pyramid_hash调用memcpy的地方都做了调用计数,分别用1行样本和5000条样本,在drop_out_percent=0. samplint_rate=1.的条件下,测了一下结果。paddle和竞品的memcpy次数几乎是一样的。从数据里可以看出,竞品同样的数据在每轮也会有波动。
GCC5.4,libc2.23 测试memcpy依然没有走进sse的版本:
paddle(设置环境变量)epoc均值 | 3个epoc | paddle(不设置环境变量)epoc均值 | 3个epoc | 备注 |
---|---|---|---|---|
242.34 | --- | 204.03 | --- | baseline PR18814@b0fe063 做了一些代码清理 |
238.8 | 241.98, 237.62, 236.97 | 200.2 | 199.80, 207.08, 193.69 | 使用python.fluid.dataset代替pyreader |
218(提速8%) | 218.02, 220.89, 217.92 | 198 | 200.80, 198.24, 197.46 | 使用dataset+memcpy写死拷贝16个,9月17日 |
227 | 228.48,225.29,229.53 | 使用dataset+memcpy写死拷贝16个,使用vtune采集 | ||
232.3(develop慢了6.5%) | 237.61,227.99,231.34 | 191.22(develop快了3.4%) | 190.54,189.68,193.45 | 使用dataset+memcpy写死拷贝16个,develop重新测,10月14日 |
225.8(提速2.8%) | 229.51,229.44,218.71 | 187.24(提速2%) | 188.37,187.05,186.32 | 使用fuse_emb_seq_pool,https://github.com/PaddlePaddle/Paddle/commit/36acfaeeda4cf2f23411d6adc15c495291d1354d |
222(提速1.7%) | 216.94,220.10,229.21 | 186(提速0.67%) | 189.68,183.48,184.79 | 去掉debug按钮:运行时不打出中间结果 |
221.67(提速0.15%) | 228.00,218.44,218.59 | 183.01(提速1.6%) | 185.73,179.46,183.85 | axpy使用avx实现,之前是sse实现 |
214.66(提速3.1%) | 221.52,206.26,216.21 | 179.72(提速1.8%) | 176.82,178.06,184.29 | 使用 @zhaoyuchen2018 优化的murmurhash3_x64_128 https://github.com/PaddlePaddle/benchmark/issues/151#issuecomment-517139284 |
大致结论:
可以看出只有一个主线程core在忙,其他占比时间都很少。
s/epoch
drop_out_percent = 0.5
,sampling_rate = 0.06
。之前采样率是1。平台 | 单线程(6148) | 24线程(6148) | 单线程(Cloud) | 24线程(Cloud) | 备注 |
---|---|---|---|---|---|
竞品 | 466 | 554 | 775 | 829 | baseline |
Paddle | 397(+14.8%) | 526(+5%) | 626(+19.2%) | 892(-7.5%) | https://github.com/PaddlePaddle/Paddle/commit/25ffa8445d0900 murmurhash3_x64_128优化 |
Paddle | 381(+18.2%) | 495(+10.6%) | https://github.com/PaddlePaddle/Paddle/commit/1cd6721873c221d4912de839b2f3bf7600a23aaa 去掉反向copy,去掉反向中的临时Tensor,memcpy写死拷贝16 | ||
Paddle | 367.6(+21.1%) | 484.7(+12.5%) | 616(+20.5%) | 872(-5.2%) | https://github.com/PaddlePaddle/Paddle/commit/b19e1a1b567a68cba48486b1cd48cd4e67dd2732 使用memcpy预取 |
由于:
每个epoch时间和profile结果也有变化。2650 V4上的profile结果见 PaddlePaddle Benchmark.pdf
Op | 占比 |
---|---|
pyramid_hash | 51.2% |
pyramid_hash_grad | 12.15% |
sum | 10.86% |
fused_embedding_seq_pool | 4.8% |
mul_grad | 4.4% |
fused_embedding_seq_pool_grad | 3.3% |
mul | 2.4% |
@luotao1 优化的目标是什么?在Clould 24线程环境下追齐竞品?892(-7.5%)
@jianhang-liu 是的。单机24线程追平竞品。
目前有以下几个可尝试的点:
@LeoZhao-Intel Please help to have a check. Thanks!
看完长长的history,感觉是在用PE做CPU上的PyramidDNN training, 有guide可以帮助我把reproduce环境setup起来吗?或者是ut ?
https://github.com/PaddlePaddle/Paddle/pull/21206 is to improve memcpy perf by prefetching
负责人
@zhaoyuchen2018 , @luotao1
初始性能
结论:
新增op