TensorFlow 網(wǎng)絡(luò)傳輸
0. 寫(xiě)在前面
tensorflow分布式訓(xùn)練時(shí),grpc的 慢 一直都被很多人所詬病。在早期的版本中,由于實(shí)現(xiàn)的一些原因,的確存在一些性能問(wèn)題(可以參見(jiàn)這個(gè) issue )。
但隨著項(xiàng)目的迭代,現(xiàn)在性能如何,就有些莫衷一是了。這里通過(guò)對(duì)兩個(gè)項(xiàng)目master分支代碼的一些測(cè)試,希望能探討下這些問(wèn)題。
1. 直觀(guān)的看傳輸速率
這里先用 一個(gè)測(cè)試程序 測(cè)試下tensor在兩個(gè)機(jī)器中的傳輸速率。測(cè)試使用的兩臺(tái)機(jī)器配置的都是萬(wàn)兆以太網(wǎng)的網(wǎng)卡:
[work@host benchtools]$ ethtool eth0
Settings for eth0:
...
Speed: 10000Mb/s
...
在兩臺(tái)機(jī)器上分別跑測(cè)試程序的worker和ps:
[host1] python tensor_transfer_throughput.py --ps_hosts=host1:12222 --worker_hosts=host2:12222 --job=ps --task=0
[host2] python tensor_transfer_throughput.py --ps_hosts=host1:12222 --worker_hosts=host2:12222 --data_mb=100 --job=worker --task=0 --iters=100
測(cè)試程序干的事情很簡(jiǎn)單:在ps和worker上各創(chuàng)建一個(gè)相同大小的variable, 然后worker反復(fù)將自己的variable assign給ps。在上面的測(cè)試中,我們將variable的大小設(shè)置為100M,傳輸次數(shù)為100。
測(cè)試結(jié)果在worker運(yùn)行結(jié)束后可以看到:
[host2] python tensor_transfer_throughput.py --ps_hosts=host1:12222 --worker_hosts=host2:12222 --data_mb=100 --job=worker --task=0 --iters=100
....
transfer rate: 173.488801 MB/s
利用ifstat工具也可以看到網(wǎng)絡(luò)的傳輸性能:
[hosts1]$ ./ifstat
eth0 eth1
KB/s in KB/s out KB/s in KB/s out
191.95 176435.6 0.00 0.00
206.18 170675.3 0.00 0.00
222.45 220156.5 0.00 0.00
162.84 169024.8 0.00 0.00
224.44 211070.7 0.00 0.00
可以看到兩種測(cè)試的througput效果差不多。理論上來(lái)說(shuō)ifstat可能會(huì)比worker的輸出稍微大一點(diǎn),因?yàn)間rpc要為每次傳輸額外添加一些header信息。但和100MB的數(shù)據(jù)相比,應(yīng)該可以忽略不計(jì)。
但無(wú)論是哪個(gè)結(jié)果,離理論值的1.25GBps(10Gbps)差距仍舊非常大。所以初步來(lái)看,網(wǎng)卡的利用率是比較低的。
2. 單獨(dú)測(cè)試grpc
為了驗(yàn)證問(wèn)題是不是出在grpc這里,我利用 另一個(gè)測(cè)試程序 ,來(lái)測(cè)試grpc本身的傳輸效率。
程序不太復(fù)雜,要點(diǎn)包括:
client和server端的功能要簡(jiǎn)單,盡量減少額外操作所帶來(lái)的時(shí)間開(kāi)銷(xiāo):client只負(fù)責(zé)無(wú)腦發(fā)送,server端也要直接丟棄收到的數(shù)據(jù)。
直接利用grpc的ByteBuffer,從而避免掉在發(fā)送和接收時(shí)的memcpy。這點(diǎn)和tensorflow發(fā)送tensor的流程也是一致的。
server端可以創(chuàng)建多個(gè)completion queue, 從而可以指定多個(gè)worker線(xiàn)程。
client利用異步接口。可以指定傳輸并發(fā)度,也可以允許grpc創(chuàng)建多個(gè)channel。
可以指定發(fā)送數(shù)據(jù)和響應(yīng)數(shù)據(jù)塊的大小。
然后將程序部署到兩臺(tái)機(jī)器上開(kāi)始測(cè)試。client每次向server發(fā)送100M數(shù)據(jù),共發(fā)送1000條:
[host1] ./grpc_raw --job_type=server --server_threads=1 --message_size=10
[host2] ./grpc_raw --job_type=client --job_type=client --target_ip=host1 --total_message=1000 --message_size=104857600
利用ifstat看結(jié)果:
[work@host2 benchtools]$ ./ifstat
eth0 eth1
KB/s in KB/s out KB/s in KB/s out
162.05 198529.9 0.00 0.00
128.67 150799.5 0.00 0.00
196.09 203136.0 0.00 0.00
169.20 192864.8 0.00 0.00
130.67 146532.7 0.00 0.00
可以看到和測(cè)tensor傳輸時(shí)類(lèi)似,也是170MBps左右,離1.25GBps的理論值也差距較大。
3. 為什么慢
為了進(jìn)一步確定問(wèn)題,我用 iperf 工具對(duì)網(wǎng)絡(luò)的throughput做了單獨(dú)的測(cè)試:
[host1] ./iperf3 -s -i 5
[host2] ./iperf3 -c host1 -i 5 -t 1000
測(cè)試結(jié)果如下:
[host2]$ ./iperf3 -c host1 -i 5 -t 1000
...
[ 5] 0.00-5.00 sec 983 MBytes 1.65 Gbits/sec 31545 2.49 MBytes
[ 5] 5.00-10.00 sec 839 MBytes 1.41 Gbits/sec 35645 889 KBytes
[ 5] 10.00-15.00 sec 830 MBytes 1.39 Gbits/sec 35863 954 KBytes
...
可以看到大概也就是1.4Gbps(175MBps)左右,和 grpc的測(cè)試結(jié)果差不多 。
為什么會(huì)這樣呢?事實(shí)上,當(dāng)提高socket數(shù)后,結(jié)果就會(huì)大大改觀(guān),總的傳輸速率會(huì)達(dá)到9.3 Gbps左右,從而和理論值接近:
[host2]$ ./iperf3 -c host1 -i 5 -t 1000 -P 8
...
[ 5] 40.00-45.00 sec 621 MBytes 1.04 Gbits/sec 9936 2.06 MBytes
....
[ 19] 40.00-45.00 sec 206 MBytes 346 Mbits/sec 922 90.5 KBytes
[SUM] 40.00-45.00 sec 5.43 GBytes 9.33 Gbits/sec 33646
這里我們可以看到的一個(gè)結(jié)論是: 單個(gè)socket可能(遠(yuǎn)遠(yuǎn))無(wú)法用滿(mǎn)網(wǎng)卡的帶寬 。
那么如果把grpc的socket數(shù)增加如何?遺憾的是,目前grpc還不支持這樣的特性。在grpc里,通信是用 channel 來(lái)進(jìn)行抽象的。 哪怕你在兩個(gè)機(jī)器間創(chuàng)建多個(gè)channel, 他們?cè)诘讓右彩菚?huì)共享socket的 。
4. 單個(gè)socket用不滿(mǎn)網(wǎng)卡?
當(dāng)我通過(guò)測(cè)試得出這個(gè)結(jié)論時(shí),我內(nèi)心也是無(wú)法接受的。我嘗試了
手動(dòng)調(diào)整擁塞窗口(事實(shí)上也沒(méi)有必要,因?yàn)門(mén)CP會(huì)自發(fā)的增大它;穩(wěn)定后的擁塞窗口大小,也沒(méi)有達(dá)到Linux的上限)。
關(guān)閉Nagel算法
傳輸速率仍然沒(méi)有變化。
后來(lái)在組里boss的建議下,我換了兩臺(tái)機(jī)器做測(cè)試。發(fā)現(xiàn)對(duì)于不同的機(jī)器組合,單socket的傳輸性能是不同的。 也存在一些機(jī)器,他們的單socket性能是可以達(dá)到網(wǎng)卡理論上限的 。
對(duì)于這一問(wèn)題,現(xiàn)在懷疑可能和網(wǎng)絡(luò)布局以及中間的交換機(jī)有關(guān)系。但具體的根源究竟是什么,還無(wú)從得知。
5. 繼續(xù)測(cè)試
在我換了 單socket可以打滿(mǎn)帶寬 的兩臺(tái)機(jī)器后,我把1和2中的實(shí)驗(yàn)使用相同的參數(shù)重新做了一遍。結(jié)論如下:
grpc在單server單client的前提下,網(wǎng)卡傳輸?shù)睦寐蔬€是非常高的。在我的實(shí)驗(yàn)中大概能到9Gbps左右,比iperf的結(jié)果稍遜一點(diǎn),目測(cè)也就是5%左右。這可能和grpc在數(shù)據(jù)傳輸時(shí)的一些數(shù)據(jù)結(jié)構(gòu)的分配、處理有關(guān),但整理來(lái)說(shuō)grpc性能已經(jīng)比較可觀(guān)了。
對(duì)于傳輸tensor的測(cè)試而言,傳輸速率大概能到5Gbps左右,是裸grpc的一多半。
這里有兩個(gè)問(wèn)題:
1. 為什么 傳輸tensor 的吞吐要低于 裸的grpc傳輸 ,問(wèn)題在哪里?
2. 在我們最開(kāi)始的兩個(gè)實(shí)驗(yàn)中,由于單socket極限帶寬較低,這二者的傳輸效率類(lèi)似。為什么提高單socket的極限帶寬后,二者開(kāi)始體現(xiàn)出差別來(lái)?
其實(shí)這兩個(gè)問(wèn)題并不難解釋?zhuān)?/p>
在傳輸tensor時(shí),除了有效的傳輸數(shù)據(jù)外,還有master驅(qū)動(dòng)worker運(yùn)行、序列化、反序列化、數(shù)據(jù)assign等其他操作。而我們測(cè)試看到的throughput,是把這些操作都當(dāng)成 有效傳輸 而平均化后的一個(gè)結(jié)果。
兩個(gè)機(jī)器間帶寬越高,額外操作的占比就越大,對(duì)總throughput的影響就越大。
6. 驗(yàn)證假設(shè)
為了驗(yàn)證我們的假設(shè),我們需要知道 tensorflow在傳輸tensor時(shí),真正用于數(shù)據(jù)傳輸?shù)臅r(shí)間是多少 ,從而可以根據(jù)數(shù)據(jù)量大致推算一下傳輸時(shí)的網(wǎng)絡(luò)帶寬。
可以先用timeline看一下每一步所有op的耗時(shí),以及RecvTensor這個(gè)op的耗時(shí)。
run_options = tf.RunOptions(trace_level=tf.RunOptions.FULL_TRACE)
run_metadata = tf.RunMetadata()
sess.run(add_op.op, options=run_options, run_metadata=run_metadata)
trace = timeline.Timeline(step_stats=run_metadata.step_stats)
trace_file = open('timeline.ctf.json', 'w')
trace_file.write(trace.generate_chrome_trace_format())
結(jié)果(dur表示op的耗時(shí),單位為us):
{
"name": "RecvTensor",
...
"dur": 183311
},
....
{
"name": "Assign",
...
"dur": 19925
}
耗時(shí)主要在RecvTensor和Assign上,總耗時(shí)有200ms左右。對(duì)于100M數(shù)據(jù)而言,這個(gè)耗時(shí)也和觀(guān)察到的5Gbps的吞吐大致吻合。
但我們?nèi)耘f不能知道真正在 傳輸 的時(shí)候帶寬能不能有效的利用。timeline所能給出的最小粒度就是op,而"RecvTensor"這個(gè)op,我們可以看到耗時(shí)是180ms左右。這比grpc的傳輸吞吐還是要低出不少來(lái)的。
我們知道,在Tensorflow中,一個(gè)RecvTensor是要分成如下幾個(gè)步驟的:
1. RecvOp的AsyncCompute,通過(guò)rendezvous接口,最終調(diào)用到grpc這一層。
2. 發(fā)起RecvTensor的請(qǐng)求,包括獲取一個(gè)grpc_remote_worker的handle,以及準(zhǔn)備RecvTensorRequest的protobuf,然后創(chuàng)建和rpc call相關(guān)的數(shù)據(jù)結(jié)構(gòu)
3. 調(diào)用grpc的API,將數(shù)據(jù)推到網(wǎng)絡(luò)引擎,發(fā)送數(shù)據(jù)。
4. server端從rendezvous_manager中獲取tensor, 并且和其他的meta信息包裝成ByteBuffer返回給客戶(hù)端。
5. 客戶(hù)端將收到的ByteBuffer反序列化成Tensor。
所以整個(gè)傳輸過(guò)程的慢,可能會(huì)慢在以下幾個(gè)地方:
1. 做準(zhǔn)備工作時(shí),一些線(xiàn)程調(diào)度或者加鎖操作帶來(lái)開(kāi)銷(xiāo)。
2. server的序列化費(fèi)時(shí)間。
3. grpc的網(wǎng)絡(luò)引擎就是慢,比如說(shuō)引入額外的數(shù)據(jù)拷貝之類(lèi)的,導(dǎo)致ByteBuffer傳輸很慢。
4. client的反序列化費(fèi)時(shí)間。
第三點(diǎn)其實(shí)不太可能,因?yàn)槲覀円呀?jīng)拿裸的grpc+ByteBuffer做過(guò)測(cè)試,其帶寬利用率是比較高的。當(dāng)然,我們也可以在Tensorflow中通過(guò)更細(xì)致的metrics來(lái)驗(yàn)證下這一點(diǎn)。
因?yàn)闆](méi)法用timeline,只能通過(guò)改tensorflow代碼來(lái)測(cè)試。為此,我簡(jiǎn)單修改了 tensorflow的代碼 ,來(lái)觀(guān)察傳輸和客戶(hù)端處理的耗時(shí)。測(cè)試的結(jié)論如下:
對(duì)于100M的tensor,grpc的傳輸?shù)臅r(shí)間大概在100ms左右。大概的數(shù)據(jù)傳輸率應(yīng)該有9Gbps左右,比較高效。
server數(shù)據(jù)序列化的時(shí)間占比很小。這點(diǎn)tensorflow的確做過(guò)專(zhuān)門(mén)處理:tensor的內(nèi)存是作為ByteBuffer直接傳輸?shù)模艽蟪潭缺苊饬藘?nèi)存拷貝。
客戶(hù)端的消息反序列化會(huì)占用一定時(shí)間,大概占到了RecvTensor的1/4多一些。主要原因是grpc ByteBuffer中的Tensor數(shù)據(jù)不滿(mǎn)足Tensor的內(nèi)存布局要求,所以必須得通過(guò)內(nèi)存拷貝來(lái)一次重新整理。
7. 擴(kuò)展性
前面分析了grpc在傳輸效率方面的性能,接下來(lái)看下有關(guān)擴(kuò)展性方面的問(wèn)題。
首先明確下,當(dāng)我們討論擴(kuò)展性時(shí),應(yīng)該從如下兩個(gè)角度來(lái)衡量:
server端未到網(wǎng)卡的瓶頸時(shí),通過(guò)增加client,server端的throughput能隨著client的個(gè)數(shù)線(xiàn)性增加。
server端達(dá)到網(wǎng)卡瓶頸后,隨著client個(gè)數(shù)的增加, server端的吞吐最好基本不會(huì)下降,而client端的latency則會(huì)線(xiàn)性的增加。
這里的測(cè)試細(xì)節(jié)就不再展開(kāi)了。通過(guò)對(duì)這兩個(gè)方面的測(cè)試,我發(fā)現(xiàn)grpc在這兩個(gè)層面基本表現(xiàn)也比較良好。
8. 總結(jié)
測(cè)試的結(jié)論大致有如下幾個(gè):
在開(kāi)發(fā)分布式程序時(shí),機(jī)房間機(jī)器的拓?fù)浣Y(jié)構(gòu)需要注意下,可能會(huì)影響單socket的極限帶寬。如果存在此類(lèi)問(wèn)題,多socket的rpc是一個(gè)可能可行的方案。
grpc在大數(shù)據(jù)包的傳輸上,帶寬利用率和擴(kuò)展性都還不錯(cuò)。
對(duì)于tensorflow的RecvTensor,收到數(shù)據(jù)后的后續(xù)處理,會(huì)占據(jù)一部分計(jì)算資源,對(duì)總體的網(wǎng)卡帶寬會(huì)存在影響。
幾個(gè)需要繼續(xù)調(diào)研的方面有:
grpc在高并發(fā)處理小數(shù)據(jù)包上latency表現(xiàn)如何,可以調(diào)研一下。對(duì)與tensorflow而言,這其實(shí)不太重要。但對(duì)于latency敏感的在線(xiàn)服務(wù)而言,還是非常重要的。
在tensor的send方這邊, tensor table 是用一個(gè)非常粗粒度的互斥鎖保護(hù)的,在RecvTensor請(qǐng)求較多時(shí)候懷疑可能會(huì)成為瓶頸(比如很多個(gè)worker的分布式訓(xùn)練)。這點(diǎn)需要拿大的訓(xùn)練場(chǎng)景測(cè)試一下。
電子發(fā)燒友App













評(píng)論