[譯]在CUDA C/C++中如何隱藏數(shù)據(jù)傳輸

本文翻譯自NVIDIA官方博客Parallel Forall,內(nèi)容僅供參考昂验,如有疑問請訪問原網(wǎng)站:https://devblogs.nvidia.com/parallelforall/how-overlap-data-transfers-cuda-cc/

上一篇博客中戈擒,我們討論了如何在主機和設備之間高效地進行數(shù)據(jù)傳輸眶明。在這篇文章中,我們將討論如何使用主機端的計算峦甩、設備端的計算以及某些情況下的主機與設備端的數(shù)據(jù)傳輸來隱藏數(shù)據(jù)傳輸赘来。要實現(xiàn)使用其他操作隱藏數(shù)據(jù)傳輸需要使用CUDA流现喳,所以首先讓我們來了解一下CUDA流。

譯者注:這里為了符合中文的習慣犬辰,我將“Overlap Data Transfers”譯為“隱藏數(shù)據(jù)傳輸”嗦篱。“overlap”幌缝,原意為重疊灸促,這里將其翻譯為隱藏,既可以表達隱藏了數(shù)據(jù)傳輸?shù)拈_銷涵卵,也可以隱含地表達重疊的意思浴栽,更加的形象貼切。但是某些地方轿偎,為了表達順暢典鸡,我也將其直接翻譯為重疊。不管翻譯成什么坏晦,只需要明白隱藏就是靠重疊來實現(xiàn)的萝玷,通過將幾種相同或不同的操作重疊,我們就可以近似地實現(xiàn)隱藏某些開銷昆婿。

CUDA流

CUDA流是由主機端發(fā)布球碉,在設備端順序執(zhí)行的一系列操作。在一個CUDA流中的操作可以保證按既定的順序執(zhí)行仓蛆,而在不同的流中的操作可以交疊執(zhí)行睁冬,有時甚至可以并發(fā)(concurrently)執(zhí)行。

默認流

所有設備操作看疙,包括核函數(shù)和數(shù)據(jù)傳輸豆拨,都運行在CUDA流中。當沒有指定使用哪個流時狼荞,就會使用默認流(也叫做“空流”辽装,null stream)帮碰。默認流不同于其他流相味,因為它是一個對于設備上操作同步的CUDA流:直到之前發(fā)布在流中的所有操作完成,默認流中的操作才會開始殉挽;默認流中的操作必須在其他流中的操作開始前完成丰涉。

請注意在2015年發(fā)布的CUDA 7引入了一個新的特性——可以在每個主機線程中使用單獨的默認流;也可以將每個線程的默認流作為普通流使用(即它們不對其他流中的操作進行同步)斯碌。詳情請閱讀這篇文章——GPU Pro Tip: CUDA 7 Streams Simplify Concurrency一死。

讓我們來一起看一個使用默認流的簡單例子,以及討論如何從主機和設備的角度分析流中操作的執(zhí)行過程傻唾。

cudaMemcpy(d_a, a, numBytes, cudaMemcpyHostToDevice);
increment<<<1,N>>>(d_a)
cudaMemcpy(a, d_a, numBytes, cudaMemcpyDeviceToHost);

在上面的代碼中投慈,從設備的角度來看承耿,所有上述三個操作都被發(fā)布在相同的流——默認流中,它們會按照發(fā)布的順序執(zhí)行伪煤。從主機的角度來看加袋,隱式的數(shù)據(jù)傳輸是同步的,而核函數(shù)啟動是異步的抱既。既然主機到設備的數(shù)據(jù)傳輸(第一行)是同步的职烧,那么等到數(shù)據(jù)傳輸完成CPU線程才會調(diào)用核函數(shù)。一旦核函數(shù)被調(diào)用防泵,CPU線程會立刻執(zhí)行到第三行蚀之,但是由于設備端的執(zhí)行順序這行的數(shù)據(jù)傳輸并不會立刻開始。

從主機的角度來看捷泞,核函數(shù)執(zhí)行的異步行為非常有利于設備和主機端的計算重疊足删。我們可以在上面的代碼中添加一些獨立的CPU計算。

cudaMemcpy(d_a, a, numBytes, cudaMemcpyHostToDevice);
increment<<<1,N>>>(d_a)
myCpuFunction(b)
cudaMemcpy(a, d_a, numBytes, cudaMemcpyDeviceToHost);

在上面的代碼中锁右,一旦increment()核函數(shù)在設備端被調(diào)用壹堰,CPU線程就會立刻執(zhí)行myCpuFunction(),這樣就實現(xiàn)了主機端myCpuFunction執(zhí)行與設備端核函數(shù)執(zhí)行的重疊骡湖。無論是主機端的函數(shù)先執(zhí)行還是設備端的核函數(shù)先執(zhí)行都不會影響之后設備到主機的數(shù)據(jù)傳輸贱纠,因為只有在核函數(shù)執(zhí)行完畢之后它才會開始。從設備的角度來看响蕴,與前一個代碼相比什么也沒有改變谆焊,設備完全不會意識到myCpuFunction()的執(zhí)行。

非默認流

非默認流在主機端聲明浦夷、創(chuàng)建辖试、銷毀的C/C++代碼如下:

cudaStream_t stream1;
cudaError_t result;
result = cudaStreamCreate(&stream1)
result = cudaStreamDestroy(stream1)

我們可以使用cudaMemcpyAsync()函數(shù)來在一個非默認流中發(fā)布一個數(shù)據(jù)傳輸,這很類似于之前博客中討論的cudaMemcpy()函數(shù)劈狐,區(qū)別就在于前者有第四個參數(shù)罐孝,用于標識使用哪個CUDA流。

result = cudaMemcpyAsync(d_a, a, N, cudaMemcpyHostToDevice, stream1)

cudaMemcpyAsync()在主機端是非同步的肥缔,所以當數(shù)據(jù)傳輸一旦開始控制權就會立刻返回到主機線程莲兢。對于2D和3D的數(shù)組的拷貝,我么可以使用cudaMemcpy2DAsync()cudaMemcpy3DAsync()的函數(shù)形式续膳。

在啟動核函數(shù)時改艇,我們需要使用第四個執(zhí)行時配置參數(shù)(三對尖括號中)——流標識符(第三個執(zhí)行時配置參數(shù)是為了分配共享內(nèi)存,我們會在之后討論坟岔,這里使用0)谒兄。

increment<<<1,N,0,stream1>>>(d_a)

流的同步

你可能會遇到需要將主機代碼與流中操作同步的情況,但是非默認流中的所有操作對于主機代碼都是非同步的社付。有好幾種方法可以解決這個問題承疲。最有力的方法是使用cudaDeviceSynchronize()邻耕,它可以阻塞主機代碼直到之前所有發(fā)布在設備端的代碼全部完成為止。在大多數(shù)情況下燕鸽,這其實都太過了赊豌,而且也會有損程序性能,因為這種方式會拖延整個設備和主機線程绵咱。

譯者注:流的同步一般被用于時間測量碘饼。

CUDA流API中有多種溫和的方式來同步主機代碼。函數(shù)cudaStreamSynchronize(流)可以用于阻塞主機線程直到之前發(fā)布在指定流的所有操作完成為止悲伶。函數(shù)cudaStreamQuery(流)可以用于測試之前發(fā)布在指定流的所有操作是否完成艾恼,但不會阻塞主機線程。函數(shù)cudaEventSynchronize(事件)和cudaEventQuery(事件)與前兩種函數(shù)很像麸锉,區(qū)別在于后者是基于指定事件是否被記錄而前者是基于指定的流是否空閑钠绍。你也可以在一個單獨的流中基于一個特定的事件使用cudaStreamWaitEvent(事件)函數(shù)(即使事件被記錄在不同的流中或者不同的設備中!)

核函數(shù)執(zhí)行和數(shù)據(jù)傳輸?shù)闹丿B

之前我們已經(jīng)演示了如何在默認流中用主機端代碼來隱藏核函數(shù)執(zhí)行花沉。但是我們的主要目的是演示如何用核函數(shù)執(zhí)行隱藏數(shù)據(jù)傳輸柳爽。要實現(xiàn)它有幾點要求:

  • 設備必須可以“并發(fā)地拷貝和執(zhí)行”。我們可以通過訪問cudaDeviceProp結構體的deviceOverlap屬性或者從CUDA SDK/Toolkit中deviceQuery示例程序的輸出中獲得碱屁。幾乎所有計算能力1.1及以上的設備都支持設備重疊磷脯。

  • 核函數(shù)執(zhí)行和數(shù)據(jù)傳輸必須在不同的非默認流中。

  • 涉及到數(shù)據(jù)傳輸?shù)闹鳈C內(nèi)存必須是固定主機內(nèi)存娩脾。

下面讓我們來修改上面的代碼以使用多個CUDA流赵誓,看一看是否實現(xiàn)了數(shù)據(jù)傳輸?shù)碾[藏。完整的代碼可以在Github上找到柿赊。在這個被修改的代碼中俩功,我們將大小為N的數(shù)組分為streamSize大小的數(shù)據(jù)塊。既然核函數(shù)可以獨立地操作所有數(shù)據(jù)碰声,那么每個數(shù)據(jù)塊也可以被獨立地處理诡蜓。流(非默認流)的數(shù)量nStreams=N/streamSize。實現(xiàn)數(shù)據(jù)的分解處理有多種方式胰挑,一種是將對每個數(shù)據(jù)塊的所有操作都放到一個循環(huán)中蔓罚,代碼如下所示:

for (int i = 0; i < nStreams; ++i) {
  int offset = i * streamSize;
  cudaMemcpyAsync(&d_a[offset], &a[offset], streamBytes, cudaMemcpyHostToDevice, stream[i]);
  kernel<<<streamSize/blockSize, blockSize, 0, stream[i]>>>(d_a, offset);
  cudaMemcpyAsync(&a[offset], &d_a[offset], streamBytes, cudaMemcpyDeviceToHost, stream[i]);
}

另一種方式是將類似的操作放在一起批處理,首先發(fā)布所有主機到設備的數(shù)據(jù)傳輸洽腺,之后是核函數(shù)執(zhí)行脚粟,然后就是設備到主機的數(shù)據(jù)傳輸,代碼如下所示:

for (int i = 0; i < nStreams; ++i) {
  int offset = i * streamSize;
  cudaMemcpyAsync(&d_a[offset], &a[offset],
                  streamBytes, cudaMemcpyHostToDevice, cudaMemcpyHostToDevice, stream[i]);
}

for (int i = 0; i < nStreams; ++i) {
  int offset = i * streamSize;
  kernel<<<streamSize/blockSize, blockSize, 0, stream[i]>>>(d_a, offset);
}

for (int i = 0; i < nStreams; ++i) {
  int offset = i * streamSize;
  cudaMemcpyAsync(&a[offset], &d_a[offset],
                  streamBytes, cudaMemcpyDeviceToHost, cudaMemcpyDeviceToHost, stream[i]);
}

上述兩種異步方法都會產(chǎn)生正確的結果蘸朋,而且同一個流中相互依賴的操作都會按照需要的順序執(zhí)行。然而扣唱,這兩種方式的性能在不同版本的GPU上具有很大的差異藕坯。在Tesla C1060的GPU(計算能力1.3)上運行上述測試代碼团南,結果如下:

Device : Tesla C1060

Time for sequential transfer and execute (ms ): 12.92381
  max error : 2.3841858E -07
Time for asynchronous V1 transfer and execute (ms ): 13.63690
  max error : 2.3841858E -07
Time for asynchronous V2 transfer and execute (ms ): 8.84588
  max error : 2.3841858E -07

在Tesla C2050(計算能力2.0),我們得到以下結果:

Device : Tesla C2050

Time for sequential transfer and execute (ms ): 9.984512
  max error : 1.1920929e -07
Time for asynchronous V1 transfer and execute (ms ): 5.735584
  max error : 1.1920929e -07
Time for asynchronous V2 transfer and execute (ms ): 7.597984
  max error : 1.1920929e -07

這里數(shù)據(jù)傳輸和核函數(shù)順序執(zhí)行的同步版本可以作為比較上述兩種異步版本是否有加速效果的基準炼彪。為什么這兩種異步執(zhí)行策略在不同架構上的效果不同呢吐根?為了解釋這一結果,我們需要了解CUDA設備如何調(diào)度和執(zhí)行任務辐马。CUDA設備中存在多種不同任務的引擎拷橘,它們會對發(fā)布的操作進行排隊。它們的功能就是維護不同引擎中任務間的依賴喜爷,但是在引擎內(nèi)部所有的外部依賴都會丟失冗疮;每個引擎中的任務都會按照它們被發(fā)布的順序執(zhí)行。C1060有一個單獨的拷貝引擎和一個單獨的核函數(shù)引擎檩帐。下圖是C1060運行上面示例代碼的時間線:

C1060 timelines

NOTE:H2D表示主機到設備术幔;D2H表示設備到主機

在這個原理圖中,我們假設主機到設備的數(shù)據(jù)傳輸湃密、核函數(shù)執(zhí)行诅挑、設備到主機三者所用的時間相同(所選擇的核函數(shù)代碼就是專門這樣設計的)。正如預料的那樣泛源,順序執(zhí)行的核函數(shù)并沒有任何操作重疊拔妥。對于異步版本1的代碼,拷貝引擎中的執(zhí)行順序是: H2D 1號流, D2H 1號流, H2D 2號流, D2H 2號流, 以此類推达箍。這就是為什么異步版本1沒有任何加速的原因:在拷貝引擎上任務的發(fā)布順序使得核函數(shù)執(zhí)行和數(shù)據(jù)傳輸無法重疊毒嫡。然而,從版本2較少的執(zhí)行時間來看幻梯,所有主機到設備的數(shù)據(jù)傳輸都在設備到主機的數(shù)據(jù)傳輸之前兜畸,是有可能實現(xiàn)重疊的。在原理圖中碘梢,我們可以看出異步版本理論時間是順序版本的8/12咬摇,前面的結果8.7ms剛好符合這個推算。

在C2050中煞躬,有兩個特征共同導致了它與C1060的性能差異肛鹏。C2050有兩個拷貝引擎,一個是用于主機到設備的數(shù)據(jù)傳輸恩沛,另一個用于設備到主機的數(shù)據(jù)傳輸在扰,第三個引擎是核函數(shù)引擎。下圖描述了C2050執(zhí)行示例代碼的時間線:

c2050 timelines

C2050具有兩個拷貝引擎恰好解釋了為什么異步版本1在C2050上具有很好的加速效果:與C1060正相反雷客,在stream[i]上設備到主機的數(shù)據(jù)傳輸并不會妨礙stream[i+1]上的主機到設備的數(shù)據(jù)傳輸芒珠,因為在C2050上每個方向的拷貝都有單獨的引擎。上面的原理圖顯示搅裙,該異步版本1的執(zhí)行時間大約是順序版本的一半皱卓,和實際結果相差無幾裹芝。

但是我們該如何解釋異步版本2在C2050上的性能下降呢?其實這與C2050可以并發(fā)執(zhí)行多個核函數(shù)有關娜汁。當多個核函數(shù)背靠背地被發(fā)布在不同的流(非默認流)中時嫂易,調(diào)度器會盡力確保這些核函數(shù)并發(fā)執(zhí)行,結果就導致每個核函數(shù)完成的信號被延遲掐禁,即所有核函數(shù)執(zhí)行完畢才發(fā)出信號怜械,而這個信號負責啟動設備到主機的數(shù)據(jù)傳輸。因此傅事,在異步版本2中缕允,主機到設備的數(shù)據(jù)傳輸與核函數(shù)執(zhí)行可以重疊,而核函數(shù)執(zhí)行與設備到主機的數(shù)據(jù)傳輸不能重疊享完。上面的原理圖中顯示異步版本2的總體時間大約是順序版本的9/12灼芭,正好與實驗結果7.5ms相吻合。

關于這個例子般又,在這篇文章CUDA Fortran Asynchronous Data Transfers中有更詳細的講解彼绷。讓人高興的是,對于計算能力3.5的設備(K20系列)茴迁,它所具有的超Q特性使得我們已經(jīng)不在需要特別安排啟動順序寄悯,所以上述兩個版本都會有很好的加速效果。我們會在將來的博客中討論如何使用開普勒的這些特性堕义。但是現(xiàn)在讓我們來看一下Tesla K20c GPU的運行結果猜旬。正如你所看到的,兩個異步執(zhí)行版本相比同步版本都有相同的加速效果倦卖。

Device : Tesla K20c
Time for sequential transfer and execute (ms): 7.101760
  max error : 1.1920929e -07
Time for asynchronous V1 transfer and execute (ms): 3.974144
  max error : 1.1920929e -07
Time for asynchronous V2 transfer and execute (ms): 3.967616
  max error : 1.1920929e -07

總結

這篇文章和之前的文章都對如何優(yōu)化主機和設備間的數(shù)據(jù)傳輸進行了討論洒擦。之前的文章強調(diào)如何盡可能減少數(shù)據(jù)傳輸?shù)热蝿盏膱?zhí)行時間,這篇文章介紹了流以及如何使用它們來隱藏數(shù)據(jù)傳輸怕膛,即并發(fā)地執(zhí)行數(shù)據(jù)拷貝和核函數(shù)熟嫩。

說到流,我必須要提醒一點:盡管使用默認流非常的方便而且代碼寫起來也很簡單褐捻,但我們還是應該使用非默認流或者CUDA 7支持的每個線程單獨的默認流掸茅。尤其是在寫庫函數(shù)時,這一點尤為重要柠逞。如果在庫函數(shù)中使用默認流昧狮,那么對于庫函數(shù)用戶就不會有機會實現(xiàn)數(shù)據(jù)傳輸和核函數(shù)執(zhí)行的重疊了。

現(xiàn)在你應該明白了如何高效地在主機和設備間傳輸數(shù)據(jù)板壮,在下一篇博客中我們開始學習如何在核函數(shù)中高效的訪問數(shù)據(jù)逗鸣。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子慕购,更是在濱河造成了極大的恐慌聊疲,老刑警劉巖茬底,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沪悲,死亡現(xiàn)場離奇詭異,居然都是意外死亡阱表,警方通過查閱死者的電腦和手機殿如,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來最爬,“玉大人涉馁,你說我怎么就攤上這事“拢” “怎么了烤送?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長糠悯。 經(jīng)常有香客問我帮坚,道長,這世上最難降的妖魔是什么互艾? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任愤估,我火速辦了婚禮绘梦,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己甥郑,他們只是感情好,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布瞒瘸。 她就那樣靜靜地躺著论皆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪假栓。 梳的紋絲不亂的頭發(fā)上寻行,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音但指,去河邊找鬼寡痰。 笑死,一個胖子當著我的面吹牛棋凳,可吹牛的內(nèi)容都是我干的拦坠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼剩岳,長吁一口氣:“原來是場噩夢啊……” “哼贞滨!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤晓铆,失蹤者是張志新(化名)和其女友劉穎勺良,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骄噪,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡尚困,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了链蕊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片事甜。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖滔韵,靈堂內(nèi)的尸體忽然破棺而出逻谦,到底是詐尸還是另有隱情,我是刑警寧澤陪蜻,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布邦马,位于F島的核電站,受9級特大地震影響宴卖,放射性物質(zhì)發(fā)生泄漏滋将。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一嘱腥、第九天 我趴在偏房一處隱蔽的房頂上張望耕渴。 院中可真熱鬧,春花似錦齿兔、人聲如沸橱脸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽添诉。三九已至,卻和暖如春医寿,著一層夾襖步出監(jiān)牢的瞬間栏赴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工靖秩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留须眷,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓沟突,卻偏偏與公主長得像花颗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子惠拭,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容