在機器學習訓練過程中,大家往往會發現IO成爲制約訓練速度提升的瓶頸。
提升訓練速度,數據傳輸是繞不開的話題。那麼GPU機器中,數據傳輸是如何做的呢?
同機的CPU和GPU之間數據如何傳輸?
同機的多卡之間數據如何傳輸?
多機的卡之間數據如何傳輸?
1、CPU和GPU之間
1)CPU->GPU
圖1 鎖頁內存
從CPU向GPU傳輸數據,最爲人熟知的就是cudaMemcpy了。
默認情況下,數據是從系統的分頁內存先到鎖頁內存,然後再到GPU顯存。因此如果顯式指定使用鎖頁內存,是可以加快數據傳輸速度的。
(鎖頁內存,在cuda編程裏使用CudaHostMalloc分配。實質上和linux的mlock系統調用一樣,就是給內存頁打上標記,不讓操作系統將其從物理內存交換到硬盤)
至於爲什麼cuda要這樣設計,個人理解是爲了實現的方便。因爲操作系統已經處理了硬盤和物理內存間的頁交換等情況,顯卡驅動只需要實現物理內存到GPU顯存這一種數據傳輸即可,不需要把操作系統內存管理的事情再做一遍。
圖2 G9機型(P40卡)上系統內存向顯存拷貝速度
2) GPU->CPU
GPU向CPU拷貝數據時,鎖頁內存同樣比分頁內存快
圖3 G9機型(P40卡)上顯存向系統內存拷貝速度
值得一提的是,適當使用pinned memory顯然可以加快IO速度。但是並不是越多越好,因爲鎖頁內存是完全獨佔住了物理內存,操作系統無法調度,可能會影響系統整體性能。
3)同一張GPU卡內部
同一張卡內兩塊顯存對拷,實測P40上高達~285GB/s。也比較接近於GPU卡本身的訪存速度
圖4 摘自P40 whitepaper
4)數據拷貝的overhead
在上面的測試數據中,可以看到傳輸數據量從1M->32M增長的過程中,測得的傳輸帶寬是有逐漸增加的。
這是因爲每次調用cuda api進行數據傳輸都有overhead,在數據量小的時候這個overhead在數據傳輸時間中的佔比就顯得很高。這也提示我們儘量合併小數據的傳輸
2、同機的GPU之間
一般可以通過cudaMemcpyPeer/cudaMemcpyPeerAsync函數進行顯存拷貝
1)cudaMemcpyPeer withoutP2P
/********代碼示例*******/
cudaSetDevice(1);
cudaMalloc((int**)&dest, bytes);
cudaSetDevice(2);
cudaMalloc((int**)&dsrc, bytes);
cudaMemcpyPeer(dest, 1, dsrc, 2, bytes);
圖5 GPU2向GPU1顯存拷貝
通過nvprof+nvpp可以看到:禁用GPU P2P時,數據是先從GPU2拷貝到系統內存(DtoH),然後再從系統內存拷貝到GPU1(HtoD)
當然,這裏是在一個進程內做GPU之間的數據拷貝。如果是2個進程分別運行在GPU1和GPU2上,那在CPU上這2個進程間可以通過共享內存或者socket通信來完成數據的拷貝。
2)cudaMemcpyPeer withP2P
/********代碼示例*******/
cudaSetDevice(1);
cudaMalloc((int**)&dest, bytes);
cudaSetDevice(2);
cudaMalloc((int**)&dsrc, bytes);
cudaDeviceEnablePeerAccess(1,0);
cudaDeviceEnablePeerAccess(2,0);
cudaMemcpyPeer(dest, 1, dsrc, 2, bytes);
圖6 GPU2向GPU1通過P2P進行顯存拷貝
啓用GPU P2P時,數據直接從GPU2拷貝到了GPU1,不再經過系統內存。
3)通過變量賦值方式傳輸數據
深度學習中,卡之間傳遞的數據其實很多都是參數數值,因此也可以直接用一個GPU內的變量給另一個GPU上的變量賦值來進行數據傳輸
/********代碼示例*******/
cudaOccupancyMaxPotentialBlockSize(&numBlocks, &blockSize, copyp2p_float);
copyp2p_float<<<numBlocks, blockSize, 0, streamToRun>>>(
(float *)dest, (float *)src, num_elems);
__global__ void copyp2p_float(float *__restrict__ dest, float const *__restrict__ src,
size_t num_elems) {
size_t globalId = blockIdx.x * blockDim.x + threadIdx.x;
size_t gridSize = blockDim.x * gridDim.x;
#pragma unroll(5)
for (size_t i = globalId; i < num_elems; i += gridSize) {
dest[i] = src[i];
}
}
圖7 GPU2向GPU1進行變量賦值
4)GPU->GPU速度測試
圖8 G9機型(P40卡)上GPU to GPU顯存拷貝
圖9 G9機型(P40卡)上GPU to GPU變量賦值
5)GPU機器架構
使用P40卡的公司某現役型號服務器拓撲結構如下
顯而易見,同一個PCIe Switch下的卡之間的數據傳輸 和 跨PCIe Switch的卡之間數據傳輸存在差異,
具體這兩種情況下數據的傳輸路徑有何不同,如何影響到傳輸速度,機智團隊會在後續文章中結合GPU架構演進進行分析。
圖10 某機型架構
3、多機的GPU之間
圖11 兩機GPU通信示意
1) NCCL性能參數
跨節點的GPU之間,數據傳輸當然要通過網絡。除了傳統的socket通信,還有GDR(GPU Direct RDMA)。關於GDR的原理,本文不贅述,可參考相關資料。
Nvidia提供了NCCL庫來方便基於GPU的集合通信,這也是目前分佈式GPU訓練必備的工具之一。目前最新的版本是NCCL_2.4.7,相比於之前版本,2.4提供了對通信方式更細粒度的控制。對性能有影響的參數主要包括:
- NCCL_IB_DISABLE爲1時禁止使用ib設備
- NCCL_P2P_LEVEL 0~5 控制在何種情況下GPU卡之間可以使用P2P
- NCCL_P2P_DISABLE=1 相當於設置NCCL_P2P_LEVEL=0,並且會被NCCL_P2P_LEVEL的值所覆蓋
- NCCL_NET_GDR_LEVEL 0~5 控制在何種情況下,跨節點的GPU卡之間可以使用GDR
- NCCL_NET_GDR_READ=0 會強制在發送數據時不使用GDR;而在爲1的時候,根據NCCL_NET_GDR_LEVEL來決定發送數據時是否使用GDR。接收數據時是否使用GDR完全由距離決定,和NCCL_NET_GDR_READ無關(參見nccl源碼transport/http://net.cc中netGetGdrSupport函數)。
- NCCL_SHM_DISABLE 在P2P不能生效的情況下,是否使用cpu的共享內存來傳輸數據。如果禁用,則使用socket通信
因爲nccl裏面以enum{ "PIX", "PXB", "PHB", "NODE", "SYS" }來描述設備(包括GPU卡和網卡)之間的”距離”,所以NCCL_P2P_LEVEL和NCCL_NET_GDR_LEVEL都有0~5這6種取值,來細粒度控制何種情況下可以使用P2P或者GDR。
圖12 LEVEL和distance的關係
對於圖10中機型來說,通過參考nccl源碼裏的pciDistance和netDistance函數,我們可以很輕鬆地寫出程序來輸出各GPU卡和網卡之間的”距離”。
表1 p2p_level用到的pciDistance
表2 net_gdr_level用到的netDistance
2)性能數據
表3 多機通信時,GPU/NIC間的通信方式
表4 不同配置下通信速度對比(以2機16張P40卡nccl_broadcast爲例,兩機間RoCEv2+100Gbps互聯)
圖13 不同傳輸方式對多機通信速度影響巨大
以上通過一些代碼分析和測試數據,介紹了實際開發中值得注意的影響GPU機器數據傳輸的因素。希望對從事分佈式訓練的同學們有一些幫助
參考資料
[1]https://docs.nvidia.com/deeplearning/sdk/nccl-developer-guide/docs/
[2]https://devblogs.nvidia.com/how-optimize-data-transfers-cuda-cc/