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會自發的增大它;穩定後的擁塞窗口大小,也沒有達到Linux的上限)。
  • 關閉Nagel演算法

傳輸速率仍然沒有變化。

後來在組裡boss的建議下,我換了兩台機器做測試。發現對於不同的機器組合,單socket的傳輸性能是不同的。也存在一些機器,他們的單socket性能是可以達到網卡理論上限的

對於這一問題,現在懷疑可能和網路布局以及中間的交換機有關係。但具體的根源究竟是什麼,還無從得知。

5. 繼續測試

在我換了單socket可以打滿帶寬的兩台機器後,我把1和2中的實驗使用相同的參數重新做了一遍。結論如下:

  1. grpc在單server單client的前提下,網卡傳輸的利用率還是非常高的。在我的實驗中大概能到9Gbps左右,比iperf的結果稍遜一點,目測也就是5%左右。這可能和grpc在數據傳輸時的一些數據結構的分配、處理有關,但整理來說grpc性能已經比較可觀了。
  2. 對於傳輸tensor的測試而言,傳輸速率大概能到5Gbps左右,是裸grpc的一多半。

這裡有兩個問題:

1. 為什麼傳輸tensor的吞吐要低於裸的grpc傳輸,問題在哪裡?

2. 在我們最開始的兩個實驗中,由於單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是要分成如下幾個步驟的:

1. RecvOp的AsyncCompute,通過rendezvous介面,最終調用到grpc這一層。

2. 發起RecvTensor的請求,包括獲取一個grpc_remote_worker的handle,以及準備RecvTensorRequest的protobuf,然後創建和rpc call相關的數據結構

3. 調用grpc的API,將數據推到網路引擎,發送數據。

4. server端從rendezvous_manager中獲取tensor, 並且和其他的meta信息包裝成ByteBuffer返回給客戶端。

5. 客戶端將收到的ByteBuffer反序列化成Tensor。

所以整個傳輸過程的慢,可能會慢在以下幾個地方:

1. 做準備工作時,一些線程調度或者加鎖操作帶來開銷。

2. server的序列化費時間。

3. grpc的網路引擎就是慢,比如說引入額外的數據拷貝之類的,導致ByteBuffer傳輸很慢。

4. client的反序列化費時間。

第三點其實不太可能,因為我們已經拿裸的grpc+ByteBuffer做過測試,其帶寬利用率是比較高的。當然,我們也可以在Tensorflow中通過更細緻的metrics來驗證下這一點。

因為沒法用timeline,只能通過改tensorflow代碼來測試。為此,我簡單修改了tensorflow的代碼,來觀察傳輸和客戶端處理的耗時。測試的結論如下:

  • 對於100M的tensor,grpc的傳輸的時間大概在100ms左右。大概的數據傳輸率應該有9Gbps左右,比較高效。
  • server數據序列化的時間佔比很小。這點tensorflow的確做過專門處理:tensor的內存是作為ByteBuffer直接傳輸的,很大程度避免了內存拷貝。
  • 客戶端的消息反序列化會佔用一定時間,大概佔到了RecvTensor的1/4多一些。主要原因是grpc ByteBuffer中的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敏感的在線服務而言,還是非常重要的。
  • 在tensor的send方這邊,tensor table是用一個非常粗粒度的互斥鎖保護的,在RecvTensor請求較多時候懷疑可能會成為瓶頸(比如很多個worker的分散式訓練)。這點需要拿大的訓練場景測試一下。

推薦閱讀:

相关文章