目標
使系統(tǒng)能夠發(fā)揮分布式的能力,即scalability.
實驗設計
假設(設定提交任務部分延遲相對于訓練是可以忽略不計的)
若有兩個Tworker和一個Pserver威蕉,每個Tworker負責5000張image的數(shù)據(jù)
則對比對象則是一個進程單獨負責訓練韧涨,負責10000張訓練數(shù)據(jù)
時間(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
采取和單進程一樣的配置
預想
可以設想彬祖,由于優(yōu)化器在Pserver上品抽,所以forward和backward的時間綜合應該要持平
對于Tworker來說:
optimizer的時間 =
pull + push + fill + extract(get_gradient_from_network) + slice_gradient
之前單進程的時間的是4.044秒
分析
可以看到這次花在訓練上的時間是60s左右桑包,比baseline慢了四倍之多。
所以現(xiàn)在需要分析是不是按我們的預想的分布來發(fā)展.
pull/push次數(shù)應該是:10000*5/15 = 3333(次)
one_batch_forward_and_backward
可以看到訓練時間的分布基本符合我們的預期烧颖,也就是說窄陡,四倍的開銷均來源于優(yōu)化器部分的實現(xiàn)。
優(yōu)化器
optimizer的時間 =
pull + push + fill + extract(get_gradient_from_network) + slice_gradient
已知baseline:4.044秒
根據(jù)上述公式我們得到:
pull(24.982) + push(3.657) + fill_and_extract(0.2) + slice_gradient(19.512) = 48.351秒
因此接下來需要詳細分析如何減小這一部分的時間消耗
PULL
1.對于pull涂圆,RPC調(diào)用润歉,目前是在本機的一對一通信颈抚,0.9秒并不是主要的部分,但可以預見若worker和server數(shù)量增加贩汉,該部分的開銷會增加匹舞,同時,若在不同機器上網(wǎng)絡延遲會進一步影響該項性能赐稽,值得以后多注意又憨,但目前不是主要目標。
2.可以看到調(diào)用了20040次構建ndarray寒匙,這讓pull成為主要的性能瓶頸躏将。那么正確的實現(xiàn)應該是如何調(diào)用呢?
可以猜測的是現(xiàn)在的實現(xiàn)是存在問題的会宪,對于目前的神經(jīng)網(wǎng)絡來說蚯窥,是6個參數(shù)塞帐,1-3層的權重和偏置巍沙,那么應該只有在第一次拿到參數(shù)時需要構建array,之后每一次都把對應key的參數(shù)賦值進array榔幸,而不是每一次都創(chuàng)建一個新的ndarray矮嫉。
3.可以看到反序列化的開銷也是占比較大的一塊,花費了3.735秒拨齐,對于這一塊挺尿,可以將python實現(xiàn)的ParserFromString替換為底層是c++實現(xiàn)的代碼,從而降低這里的時間占比
Slice Gradient
分析
1.可以看到這里也有和pull一樣的問題,大量的時間花在對于每一個參數(shù)數(shù)值的填充上窄俏,可以改成在第一次時創(chuàng)建相應的梯度PB對象碘菜,之后維護PB對象忍啸,對其值進行更新,而不是每一次都創(chuàng)建PB對象计雌,再構造一個結(jié)構無異的梯度組凿滤。
2.替換python實現(xiàn)的protobuf函數(shù)為底層c++實現(xiàn)的代碼
PUSH
push端的性能問題主要在Pserver端如何相應RPC調(diào)用上,因此接下來轉(zhuǎn)入C++實現(xiàn)的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導致時間大量的消耗反番,同時對于參數(shù)更新不應該在push函數(shù)內(nèi)進行更新叉钥,可以啟動一個(detach)線程在背后去更新投队,然后立即返回