天天看點

《OpenACC并行程式設計實戰》—— 第2章 OpenACC概覽 2.1 OpenACC規範的内容

本節書摘來自華章出版社《openacc并行程式設計實戰》一 書中的第1章,第1.2節,作者何滄平,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。

2007年出現的cuda c/c++語言引爆了gpu通用計算熱潮,但程式設計比較麻煩,挖掘硬體性能需要很多高超的優化技巧。為了降低程式設計門檻,2011年11月,cray、pgi、caps和英偉達4家公司聯合推出openacc 1.0程式設計标準,2012年3月pgi率先推出支援openacc的編譯器pgi accelerator with openacc。pgi公司創立于1989年,是一家在高性能計算領域很有名望的編譯器和工具供應商,屬于意法半導體旗下的全資子公司。2013年6月,openacc 2.0标準釋出。2013年7月,英偉達收購了pgi公司,但pgi原有品牌和體系得以保留并繼續正常營運,openacc、cuda fortran、cuda x86、gpgpu等相關技術的開發工作也将繼續。openacc 2.0版本功能已經相當完備,直到2015年11月才推出openacc 2.5版本。2013年11月,gcc加入openacc組織,2016年5月推出的gcc 6.1支援openacc 2.0a标準。

openacc組織的成員均為知名企業、高校、科研機構,名單見表2.1。

雖然gcc也支援openacc标準,但對最新标準的支援比較慢,支援的特性也比較少,最大的優勢是免費。pgi編譯器對openacc的支援最快、最完善,實際上openacc标準的制定人員和pgi員工有很大的重疊。該編譯器每月更新1次,每個賬戶每半年有1個月的試用時間。對教育使用者收費較低,對企業使用者收費較高。目前openacc使用者中嘗鮮者較多,學生使用者和科學家購買的動力不足,不利于推廣。是以,英偉達于2015年7月釋出了全新openacc套件,向學術開發者和研究人員免費提供openacc編譯器(其實還是pgi編譯器包裝的),同時向商業使用者提供90天免費試用版。

如前所述,openacc并行化的方式不是重寫程式,而是在串行c/c++或fortran代碼上添加一些編譯标記。支援openacc的編譯器能夠看懂這些标記,并根據标記含義将代碼編譯成并行程式。對英偉達gpu來說,編譯器将c/c++/fortran代碼翻譯成cuda c/c++代碼,然後編譯連結成并行程式。對amd gpu來說,中間代碼是opencl。在openacc語境中,cpu稱為主機(host),gpu等加速器稱為裝置(device)。這些術語的使用場景沒有嚴格規定,能準确表達含義即可,本書可能會将名詞cpu、主機、gpu、加速器、裝置混用。

程式并行化主要包含三方面的工作(表2.2):計算并行化、資料管理、運作時庫和環境變量。

并行化的唯一目的是充分利用硬體資源來提高程式運作速度,縮短運作時間。程式中最耗時間的是循環,計算并行化的目标是将循環疊代步分散到多個不同的線程上執行,這些線程運作在多個加速器核心上,進而将計算任務由cpu轉移到加速器上,減輕cpu的負擔。循環并行化需要解決的問題有這些:指定将哪個循環并行化,以什麼樣的方式組織并行線程。openacc使用計算構件kernels或parallel來完成這個工作,看起來是這個樣子(此處不必深究文法,後有詳述):

資料管理占用openacc規範的大量篇幅,文法也多。資料管了解決的問題是:如何在主機記憶體與裝置記憶體之間傳遞資料,如何開辟、釋放裝置記憶體,如何管理變量的生存期和作用域。

openacc運作時庫包含幾十個函數,這些函數的功能隻有在程式運作時才能實作,在編譯階段不能實作。例如,從幾個裝置中選取目前裝置,初始化裝置,配置設定、釋放裝置記憶體,在主機記憶體與裝置記憶體之間複制資料,等待某個操作的完成。openacc規範中規定了幾個環境變量,用來指定裝置類型和裝置編号,詳見6.6.1節。

openacc将串行程式并行化的手段是添加一些c/c++預處理語句或形式特殊的fortran注釋,預處理語句和特殊注釋分為directive和clause兩類。

directive表示主要功能,每句有且隻能有一個,作用是給編譯器一些指導,指出哪些代碼需要并行化、需要怎麼并行化,編譯器根據程式員的指導資訊生成最佳的并行代碼。

clause表示對directive的修飾,每句可以有零個或多個。

一個directive和若幹個clause就構成一個功能子產品construct。

為友善閱讀、交流,本書将directive翻譯為導語,将clause翻譯為子語,将construct翻譯為構件。網絡上有人将directive翻譯為編譯制導語句、編譯指導語句、指令語、指令等,意思都近似,但編譯制導語句、編譯指導語句太長,使用不便,指令語、指令中的指令一詞太普通,易混,且隻有強制的含義,沒有指導的含義,不太準确。導語一詞長度、意思都比較合适。clause是導語的修飾部分,更詳細地表明導語的意圖。有人将clause翻譯為子句,而子句一詞含有小句子的意思,實際上openacc中的clause都隻有一個詞,很短,不能稱為一個句子。子語一詞既說明了它與導語的關系,又有一個相同的語字,讀起來順口。construct翻譯為構件,取自建築名詞,實際功能也很相像。

科學和工程計算領域的大量曆史遺留程式絕大部分用c/c++和fortran語言開發,這三種語言也非常适合計算密集型的高性能計算應用,是以openacc目前支援c/c++和fortran。本書中示例代碼均給出c和fortran兩種版本,講述以c版本代碼為主,fortran版隻列出代碼,特殊情況下才詳細講解。對科學與工程計算而言,大部分的密集計算任務用c語言就可以完成,c++中的複雜類操作隻占用少量運作時間,是以隻有在絕對必要的時候才用c++。對fortran而言,fortran 77固定格式很少用于開發新程式,fortran 90/95/2003/2008自由格式應用廣泛,是以openacc對fortran 90及以後标準支援得更好一些,本書的示例也采用fortran自由格式。

市面上的加速器産品多種多樣,架構設計也有很大差别。為了能相容盡可能多的加速器,openacc定義了一個抽象的加速器模型,以涵蓋市場上主流加速器的特點,然後在抽象模型上建立計算執行模型。在抽象模型中(圖2.1),主機可以直接通路主機記憶體,裝置可以直接通路裝置記憶體,主機能夠配置設定、釋放裝置記憶體,主機能夠啟動裝置上的函數。但是主機不能直接通路裝置記憶體,裝置也不能直接通路主機記憶體。裝置記憶體中的資料需要在裝置運算開始之前從主機記憶體複制到裝置記憶體,裝置運算完成後再将結果複制回主機記憶體。

可以比照英偉達gpu來了解這個抽象加速器模型,實際上這個抽象模型就是在它的基礎上設計的。裝置與裝置記憶體通常離得很近(例如在同一塊顯示卡上),帶寬最大。主機與主機記憶體距離也比較近,帶寬次大。主機記憶體與裝置記憶體距離較遠,連接配接帶寬最小。英偉達gpu這樣的産品稱為分離記憶體裝置

amd apu這樣的産品中,主機記憶體和裝置記憶體共用一塊實體空間,它們之間可以共享資料,不需要複制搬遷。這類産品稱為共享記憶體裝置。

本節描述的存儲模型非常概括,初次閱讀不強求完全了解,等讀完第4章後再讀本節就會豁然開朗。

一個僅在主機上運作的程式與一個在主機+加速器上運作的程式,它們最大的差別在于加速器上的記憶體可能與主機記憶體完全分離。例如目前大多數gpu就是這樣。這種情況下,裝置記憶體可能無法被主機線程直接讀寫,這是因為它沒有被映射到主機線程的虛拟存儲空間。主機記憶體與裝置記憶體間的所有資料移動必須由主機線程完成,主機線程通過系統調用在互相分離的記憶體之間顯式地移動資料。資料移動通常采用直接記憶體通路(direct memory access,dma)技術。不能假定加速器能讀寫主機記憶體,雖然有些加速器裝置支援這樣的操作,但常常有嚴重的性能損失。

在cuda c和opencl等低層級加速器程式設計語言中,主機和加速器存儲器分離的概念非常明确,記憶體間移動資料的語句甚至占據大部分使用者代碼。在openacc模型中,記憶體間的資料移動可以是隐式的,編譯器根據程式員的導語管理這些資料移動。然而程式員必須了解背後這些互相分離的記憶體,理由包括但不限于以下幾方面。

有效加速一個區域的代碼需要較高的計算密度,而計算密度的高低取決于主機記憶體與裝置記憶體的存儲帶寬;計算密度可以用計算量除以資料量來衡量,這個商值越大,計算密度越高。

與主機記憶體相比,裝置記憶體空間有限,是以操作大量資料的代碼不能解除安裝到裝置上。在高性能叢集典型配置下,主機記憶體為128gb或256gb,而gpu上的裝置記憶體最大24gb,差一個數量級。

主機上的指針裡儲存的主機位址可能僅在主機上可用;裝置上的指針裡儲存的位址可能僅在裝置上可用。建議不要在主機記憶體與裝置記憶體之間顯式地傳遞指針的值。主機指針在裝置上解引用或裝置指針在主機上解引用很可能出錯。

openacc通過裝置資料環境來暴露互相分離的記憶體。裝置資料有一個顯式生存期,從配置設定空間直到被删除。如果裝置與本地線程共享實體記憶體或虛拟記憶體,那麼本地線程也能共享裝置資料環境。這種情況下,編譯器不必為裝置建立新的資料副本,也不需要移動資料。如果裝置記憶體與本地線程的記憶體實體地或虛拟地分離,那麼編譯器将在裝置記憶體中建立新的資料副本并将資料從本地記憶體複制到裝置環境中。

一些加速器(例如目前的gpu)使用一個較弱的存儲模型。這種模型不支援不同線程上操作的記憶體一緻性,甚至,在同一個執行單元上,隻有在存儲操作語句之間顯式地記憶體欄栅才能保證記憶體一緻性。否則,如果一個線程更新一個記憶體位址而另一個線程讀取同一個位址,或者兩個操作向同一個位置存入資料,那麼硬體可能不保證每次運作都能得到相同的結果。盡管編譯器可以檢測到一些這樣的潛在錯誤,但仍有可能編寫出一個産生不一緻數值結果的加速器parallel區域或kernels區域。

目前,一些加速器有一塊軟體管理的緩存,一些加速器有多塊硬體管理的緩存。大多數加速器具有僅在特定情形下使用的硬體緩存,并且僅限于存放隻讀資料。在cuda c和opencl等低級語言的程式設計模型中,這些緩存交由程式員管理。在openacc模型中,編譯器會根據程式員的訓示來管理這些緩存。

openacc編譯器的執行模型是主機指導加速器裝置(如gpu)的運作。主機執行使用者應用的大部分代碼,并将計算密集型區域解除安裝到加速器上執行,這些計算密集區域通常是循環。裝置上用計算構件parallel或kernels将這些循環并行化。兩個計算構件的行為稍有差别,後文會詳述。即使在加速器負責的區域,主機也必須精心安排程式的運作:在加速器裝置上配置設定存儲空間、初始化資料傳輸、将代碼發送到加速器上、給計算區域傳遞參數、為裝置端代碼排隊、等待完成、将結果傳回主機、釋放存儲空間。大多數時候,主機可以将裝置上的所有操作排成一隊,一個接一個地順序執行。然後在裝置上執行parallel區域和kernels區域,parallel區域通常包含一個或多個工作分攤(work-sharing)循環,kernels區域通常包含一個或多個被作為裝置上核心執行的循環。

目前大多數的加速器支援二到三層并行。大部分加速器支援粗粒度并行:在執行單元層次并行執行。加速器可能有限支援粗粒度并行操作之間的同步。許多加速器也支援細粒度并行:在單個執行單元上執行多個線程,這些線程可以快速地切換,進而可以忍受長時間的存儲操作延時。大多數加速器也支援每個執行單元内的單指令多資料(single instruction multiple data, simd)操作或向量操作。該執行模型表明裝置有多個層次的并行,是以程式員需要了解它們之間的差別。例如,一個完全并行的循環和一個可向量化但要求語句間同步的循環之間的差別。一個完全并行的循環可以用粗粒度并行執行。有依賴關系的循環要麼适當分割以允許粗粒度并行,要麼在單個執行單元上以細粒度并行、向量并行或串行執行。

與三個并行層次相對應,openacc設計了gang、worker和vector,見圖2.2。gang并行是粗粒度并行,加速器上将啟動許多個gang。每一個gang都将有一個或多個worker,一個worker内的simd操作或向量操作是vector并行。gang、worker、vector都是一維的,沒有二維或三維形式。

圖2.2中,worker裡的每一個小方塊代表一個vector通道(vector lane),圖中的vector長度為4,實際程式中可能會是其他值;若幹worker組成一個gang,一個計算構件可以同時啟動多個gang。用openacc的術語來說,gang對應于英偉達gpu的流式多處理器(sm或smx),vector通道對應于gpu核心,worker沒有明确的對應硬體。用cuda c/c++術語來說,gang對應block,worker和vector與線程的對應關系不确定,會根據block的一維、二維、三維組織情況而變化,第3章會有例子詳

解。

本節接下來的内容可能較難了解,這是因為它們來自openacc規範,概括性強,初次閱讀時不必深究,可以讀完全書以後回過頭來慢慢領會。

執行一個計算區域時,裝置會啟動一個或多個gang,每個gang都包含一個或多個worker,而每個worker可能還有能力執行一個或多個vector通道。開始執行時,多個gang處于gang備援模式:每個gang中的每一個worker的每一個vector通道都備援地執行相同的代碼。當到達一個标記為gang層次工作分攤的循環或嵌套循環時,程式開始以gang分裂模式執行,這個循環或嵌套循環的所有疊代步都将分裂,然後配置設定給各個gang,以實作真正的并行執行,但是每個活動的gang内僅有一個worker,并且一個worker内僅有一個vector通道。

當隻有一個活動worker時,無論是在gang備援模式中還是在gang分裂模式中,程式都處于worker單獨模式。當隻有一個活動vector通道時,程式就處于vector單獨模式。當一個gang到達标記為worker層次工作分攤的循環或嵌套循環時,這個gang就轉換為worker分裂模式,進而激活這個gang中的所有worker。循環或嵌套循環的所有疊代步分裂配置設定給這個gang中的各個worker。如果一個循環同時被标記為gang分裂和worker分裂,那麼循環裡的所有疊代步将分散到所有gang的所有worker上。如果一個worker到達一個标記為vector層次并行的循環或嵌套循環,那麼這個worker将轉換為vector分裂模式。與worker分裂模式類似,轉換為worker分裂模式将激活這個worker中的所有vector通道。使用向量操作或simd操作時,這個循環或嵌套循環的所有疊代步将分散給所有vector通道。單個循環可以被标記為gang并行、worker并行、vector并行中的一種、二種或三種,相應地,所有的疊代步會被酌情分散到所有的gang、worker、vector之上。程式員可以手動地指定使用哪些并行層次以及使用多少個gang、worker、vector,但不一定是最優的。程式員不手動指定時,編譯器會選擇它自認為最優的并行方式。

主機程式以單線程開始執行,這個線程可以用openmp程式設計接口之類的工具衍生出更多線程。在加速器上,單個gang的單個worker的單個vector通道稱為一個線程;在裝置上執行時,程式會建立一個并行執行上下文,該上下文可能包含很多這樣的線程。

程式員不要試圖在任何gang之間、worker之間或vector之間使用障礙同步、臨界區域或鎖。執行模型允許編譯器将一些gang執行完後再開始執行其他gang,這意味着在gang之間實施同步操作很可能會失敗。特别是,gang之間的障礙操作無法以可移植的方式實作,因為所有的gang可能永遠不會在同一時刻處于活動狀态。相似地,執行模型允許編譯器執行完一個gang中的一些worker或一個worker中的一些vector通道之後,再開始執行其他的worker或vector通道。也允許編譯器将一些worker或vector通道挂起,直到其他worker或vector通道執行完畢。這意味着在worker或vector通道之間實施同步操作很可能會失敗。如果使用原子操作和一個忙碌-等待循環來在worker或vector通道間實作一個障礙或關鍵區域可能永遠不會成功,這是因為排程器可能将擁有鎖的worker或vector通道挂起,導緻等待這個鎖的worker或vector通道永遠無法完成。

在某些裝置上,加速器也可以建立和啟動并行核心,并允許嵌套并行。這種情況下,openacc導語可以被一個主機線程執行,也可以被一個加速器線程執行。openacc規範使用術語本地線程和本地記憶體來表示執行導語的線程和與該線程關聯的記憶體,無論該線程是在主機上還是在加速器上。

相對于主機線程,大多數加速器可以異步操作。對這種裝置,加速器有一個或多個活動隊列。主機線程将資料搬移、過程執行等操作壓入活動隊列。壓入操作完成後,當裝置正在獨立、異步地工作時,主機線程繼續向後執行。主機線程可以查詢活動隊列狀态并等待某個隊列的所有操作完成。某個活動隊列上的操作完成後,才會執行同一隊列上的下一個操作;不同活動隊列上的操作可以同時處于活動狀态,并且可以以任意順序完成。

繼續閱讀