目标
使系统能够发挥分布式的能力,即scalability.
实验设计
假设(设定提交任务部分延迟相对于训练是可以忽略不计的)
若有两个Tworker和一个Pserver,每个Tworker负责5000张image的数据
则对比对象则是一个进程单独负责训练,负责10000张训练数据
时间(time)和计算资源(cpu使用率)的分布会是什么样呢?
单进程实验
epoch:5
mini_batch_size:15
training_data_size:10000
evaluation_data_size:500
可以看到训练时间占了几乎百分之百的部分
其中train:14.988s, evaluation:0.598s
那么训练中的时间是如何分布的呢?
至此我们可以以此为baseline,先初步观察一下单Pserver单Tworker的时间分布
单Pserver单Tworker
epoch:5
mini_batch_size:15
training_data_size:10000
evaluation_data_size:500
采取和单进程一样的配置
预想
可以设想,由于优化器在Pserver上,所以forward和backward的时间综合应该要持平
对于Tworker来说:
optimizer的时间 =
pull + push + fill + extract(get_gradient_from_network) + slice_gradient
之前单进程的时间的是4.044秒
分析
可以看到这次花在训练上的时间是60s左右,比baseline慢了四倍之多。
所以现在需要分析是不是按我们的预想的分布来发展.
pull/push次数应该是:10000*5/15 = 3333(次)
one_batch_forward_and_backward
可以看到训练时间的分布基本符合我们的预期,也就是说,四倍的开销均来源于优化器部分的实现。
优化器
optimizer的时间 =
pull + push + fill + extract(get_gradient_from_network) + slice_gradient
已知baseline:4.044秒
根据上述公式我们得到:
pull(24.982) + push(3.657) + fill_and_extract(0.2) + slice_gradient(19.512) = 48.351秒
因此接下来需要详细分析如何减小这一部分的时间消耗
PULL
1.对于pull,RPC调用,目前是在本机的一对一通信,0.9秒并不是主要的部分,但可以预见若worker和server数量增加,该部分的开销会增加,同时,若在不同机器上网络延迟会进一步影响该项性能,值得以后多注意,但目前不是主要目标。
2.可以看到调用了20040次构建ndarray,这让pull成为主要的性能瓶颈。那么正确的实现应该是如何调用呢?
可以猜测的是现在的实现是存在问题的,对于目前的神经网络来说,是6个参数,1-3层的权重和偏置,那么应该只有在第一次拿到参数时需要构建array,之后每一次都把对应key的参数赋值进array,而不是每一次都创建一个新的ndarray。
3.可以看到反序列化的开销也是占比较大的一块,花费了3.735秒,对于这一块,可以将python实现的ParserFromString替换为底层是c++实现的代码,从而降低这里的时间占比
Slice Gradient
分析
1.可以看到这里也有和pull一样的问题,大量的时间花在对于每一个参数数值的填充上,可以改成在第一次时创建相应的梯度PB对象,之后维护PB对象,对其值进行更新,而不是每一次都创建PB对象,再构造一个结构无异的梯度组。
2.替换python实现的protobuf函数为底层c++实现的代码
PUSH
push端的性能问题主要在Pserver端如何相应RPC调用上,因此接下来转入C++实现的Pserver端的性能分析
//遍历key-gradient pair
for(size_t i = 0; i < size; i++){
const task::KeyValuePair& pair = kth_gradient.pairs(i);
const uint32_t pair_size = pair.values_size();
vector<uint32_t> shape{pair_size};
gradients_[pair.name()] = make_shared<VectorParameter>(1, shape);
for(int j = 0; j < pair_size; j++){
gradients_[pair.name()]->values[j] = pair.values(j);
}
}
//update
//TODO:
//交给线程在背后detach去做
update_parameter();
可以看到有同样的问题,在响应push请求时,不断的构造出了新的array或者Matrix导致时间大量的消耗,同时对于参数更新不应该在push函数内进行更新,可以启动一个(detach)线程在背后去更新,然后立即返回