0. 寫在前面
tensorflow分布式訓練時,grpc的慢一直都被很多人所詬病。在早期的版本中,由于實現的一些原因,的確存在一些性能問題(可以參見這個issue)。
但隨著項目的迭代,現在性能如何,就有些莫衷一是了。這里通過對兩個項目master分支代碼的一些測試,希望能探討下這些問題。
1. 直觀的看傳輸速率
這里先用一個測試程序測試下tensor在兩個機器中的傳輸速率。測試使用的兩臺機器配置的都是萬兆以太網的網卡:
[work@host benchtools]$ ethtool eth0
Settings for eth0:
...
Speed: 10000Mb/s
...
在兩臺機器上分別跑測試程序的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
測試程序干的事情很簡單:在ps和worker上各創建一個相同大小的variable, 然后worker反復將自己的variable assign給ps。在上面的測試中,我們將variable的大小設置為100M,傳輸次數為100。
測試結果在worker運行結束后可以看到:
[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工具也可以看到網絡的傳輸性能:
[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
可以看到兩種測試的througput效果差不多。理論上來說ifstat可能會比worker的輸出稍微大一點,因為grpc要為每次傳輸額外添加一些header信息。但和100MB的數據相比,應該可以忽略不計。
但無論是哪個結果,離理論值的1.25GBps(10Gbps)差距仍舊非常大。所以初步來看,網卡的利用率是比較低的。
2. 單獨測試grpc
為了驗證問題是不是出在grpc這里,我利用另一個測試程序,來測試grpc本身的傳輸效率。
程序不太復雜,要點包括:
- client和server端的功能要簡單,盡量減少額外操作所帶來的時間開銷:client只負責無腦發送,server端也要直接丟棄收到的數據。
- 直接利用grpc的ByteBuffer,從而避免掉在發送和接收時的memcpy。這點和tensorflow發送tensor的流程也是一致的。
- server端可以創建多個completion queue, 從而可以指定多個worker線程。
- client利用異步接口。可以指定傳輸并發度,也可以允許grpc創建多個channel。
- 可以指定發送數據和響應數據塊的大小。
然后將程序部署到兩臺機器上開始測試。client每次向server發送100M數據,共發送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看結果:
[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
可以看到和測tensor傳輸時類似,也是170MBps左右,離1.25GBps的理論值也差距較大。
3. 為什么慢
為了進一步確定問題,我用iperf工具對網絡的throughput做了單獨的測試:
[host1] ./iperf3 -s -i 5
[host2] ./iperf3 -c host1 -i 5 -t 1000
測試結果如下:
[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的測試結果差不多。
為什么會這樣呢?事實上,當提高socket數后,結果就會大大改觀,總的傳輸速率會達到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
這里我們可以看到的一個結論是:單個socket可能(遠遠)無法用滿網卡的帶寬。
那么如果把grpc的socket數增加如何?遺憾的是,目前grpc還不支持這樣的特性。在grpc里,通信是用channel來進行抽象的。哪怕你在兩個機器間創建多個channel, 他們在底層也是會共享socket的。
4. 單個socket用不滿網卡?
當我通過測試得出這個結論時,我內心也是無法接受的。我嘗試了手動調整擁塞窗口(事實上也沒有必要,因為tcp會自發的增大它)、關閉Nagel算法后,傳輸速率仍然沒有變化。
后來在組里boss的建議下,我換了兩臺機器做測試。發現對于不同的機器組合,單socket的傳輸性能是不同的。也存在一些機器,他們的單socket性能是可以達到網卡理論上限的。
對于這一問題,現在懷疑可能和網絡布局以及中間的交換機有關系。但具體的根源究竟是什么,還無從得知。
5. 繼續測試
在我換了單socket可以打滿帶寬的兩臺機器后,我把1和2中的實驗使用相同的參數重新做了一遍。結論如下:
- grpc在單server單client的前提下,網卡傳輸的利用率還是非常高的。在我的實驗中大概能到9Gbps左右,比iperf的結果稍遜一點,目測也就是5%左右。這可能和grpc在數據傳輸時的一些數據結構的分配、處理有關,但整理來說grpc性能已經比較可觀了。
- 對于傳輸tensor的測試而言,傳輸速率大概能到5Gbps左右,是裸grpc的一多半。
這里有兩個問題:
- 為什么傳輸tensor的吞吐要低于裸的grpc傳輸,問題在哪里?
- 在我們最開始的兩個實驗中,由于單socket極限帶寬較低,這二者的傳輸效率類似。為什么提高單socket的極限帶寬后,二者開始體現出差別來?
其實這兩個問題并不難解釋:
- 在傳輸tensor時,除了有效的傳輸數據外,還有master驅動worker運行、序列化、反序列化、數據assign等其他操作。而我們測試看到的throughput,是把這些操作都當成有效傳輸而平均化后的一個結果。
- 兩個機器間帶寬越高,額外操作的占比就越大,對總throughput的影響就越大。
6. 驗證假設
為了驗證我們的假設,我們需要知道tensorflow在傳輸tensor時,真正用于數據傳輸的時間是多少,從而可以根據數據量大致推算一下傳輸時的網絡帶寬。
可以先用timeline看一下每一步所有op的耗時,以及RecvTensor這個op的耗時。
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())
結果(dur表示op的耗時,單位為us):
{
"name": "RecvTensor",
...
"dur": 183311
},
....
{
"name": "Assign",
...
"dur": 19925
}
耗時主要在RecvTensor和Assign上,總耗時有200ms左右。對于100M數據而言,這個耗時也和觀察到的5Gbps的吞吐大致吻合。
但我們仍舊不能知道真正在傳輸的時候帶寬能不能有效的利用。timeline所能給出的最小粒度就是op,而"RecvTensor"這個op,我們可以看到耗時是180ms左右。這比grpc的傳輸吞吐還是要低出不少來的。
我們知道,在Tensorflow中,一個RecvTensor是要分成如下幾個步驟的:
- RecvOp的AsyncCompute,通過rendezvous接口,最終調用到grpc這一層。
- 發起RecvTensor的請求,包括獲取一個grpc_remote_worker的handle,以及準備RecvTensorRequest的protobuf,然后創建和rpc call相關的數據結構
- 調用grpc的API,將數據推到網絡引擎,發送數據。
- server端從rendezvous_manager中獲取tensor, 并且和其他的meta信息包裝成ByteBuffer返回給客戶端。
- 客戶端將收到的ByteBuffer反序列化成Tensor。
所以整個傳輸過程的慢,可能會慢在以下幾個地方:
- 做準備工作時,一些線程調度或者加鎖操作帶來開銷。
- server的序列化費時間。
- grpc的網絡引擎就是慢,比如說引入額外的數據拷貝之類的,導致ByteBuffer傳輸很慢。
- client的反序列化費時間。
第三點其實不太可能,因為我們已經拿裸的grpc+ByteBuffer做過測試,其帶寬利用率是比較高的。當然,我們也可以在Tensorflow中通過更細致的metrics來驗證下這一點。
因為沒法用timeline,只能通過改tensorflow代碼來測試。為此,我簡單修改了tensorflow的代碼,來觀察傳輸和客戶端處理的耗時。測試的結論如下:
- 對于100M的tensor,grpc的傳輸的時間大概在100ms左右。大概的數據傳輸率應該有9Gbps左右,比較高效。
- server數據序列化的時間占比很小。這點tensorflow的確做過專門處理:tensor的內存是作為ByteBuffer直接傳輸的,很大程度避免了內存拷貝。
- 客戶端的消息反序列化會占用一定時間,大概占到了RecvTensor的1/4多一些。主要原因是Message中的Tensor數據不滿足Tensor的內存布局要求,所以必須得通過內存拷貝來一次重新整理。
7. 擴展性
前面分析了grpc在傳輸效率方面的性能,接下來看下有關擴展性方面的問題。
首先明確下,當我們討論擴展性時,應該從如下兩個角度來衡量:
- server端未到網卡的瓶頸時,通過增加client,server端的throughput能隨著client的個數線性增加。
- server端達到網卡瓶頸后,隨著client個數的增加, server端的吞吐最好基本不會下降,而client端的latency則會線性的增加。
這里的測試細節就不再展開了。通過對這兩個方面的測試,我發現grpc在這兩個層面基本表現也比較良好。
8. 總結
測試的結論大致有如下幾個:
- 在開發分布式程序時,機房間機器的拓撲結構需要注意下,可能會影響單socket的極限帶寬。如果存在此類問題,多socket的rpc是一個可能可行的方案。
- grpc在大數據包的傳輸上,帶寬利用率和擴展性都還不錯。
- 對于tensorflow的RecvTensor,收到數據后的后續處理,會占據一部分計算資源,對總體的網卡帶寬會存在影響。
幾個需要繼續調研的方面有:
- grpc在高并發處理小數據包上latency表現如何,可以調研一下。對與tensorflow而言,這其實不太重要。但對于latency敏感的在線服務而言,還是非常重要的。
- send方rendezvous中,tensor table用的是非常粗粒度的互斥鎖,在RecvTensor請求較多時候懷疑可能會成為瓶頸(比如很多個worker的分布式訓練)。需要拿大的訓練場景測試一下。