PU架構具備高處理器數量和記憶體頻寬,適合處理大量密集運算任務,並使用SIMD和SIMT架構來提高運算效率。異質運算結合CPU和GPU的優勢,需透過編譯器將高階語言轉換為低階二進制碼以確保相容性。
高效能運算成為AI時代的技術發展火車頭,繼上一篇討論平行運算(Parallel Computing)的概念、共享式(Shared)與分散式(Distributed)記憶體架構之差異,本文將介紹可向量化、GPU的架構、跨平台的異質運算(Heterogeneous Computing),以及能將GPU的硬體架構原則映射到應用程式的Kokkos編碼框架(Coding Framework)。
可向量化
可向量化是使用向量指令(Vector Instruction)同時執行多個作業的過程。向量化是平行化的一種程式設計形式。為了實現可向量化,循環迴圈之間必須彼此獨立,互不相關。當資料互相獨立時,編寫可向量化程式碼就變得很簡單。如式子(1)中,在for循環迴圈中,每一次的for迴圈都是獨立的,這意味著a(i)的處理獨立於a(i+1)。因此,此程式碼是可向量化的,且陣列a可以使用陣列b和c中的元素,平行地計算出陣列a的多個元素值。
圖說
編譯器(Compiler)通常都能夠分析此類循環運算,並將其轉換為向量作業序列。但是,當一個迴圈中的作業依賴於前一個迴圈的運算結果時,就會出現問題。在這種情況下,自動向量化可能會導致錯誤的結果,這就稱為資料依賴性(Data Dependency)。在循環運算中,常見的資料之間的獨立或依賴關係,有下列幾種:
先寫後讀(Read After Write, RAW):不可向量化,如式子(2)。
圖說
圖說
- 先讀後寫(Write After Read, WAR):可向量化,如式子(3)。
圖說
圖說
- 寫入後寫入(Write After Write, WAW):不可向量化,如式子(4)。
圖說
圖說
- 讀出後讀出(Read After Read, RAR):可向量化,如式子(5)。
圖說
圖說
向量化的規則必須被遵守,例如:確保循環迴圈中的每一變數值都是各自獨立的、避免隨機的資料存取,以及防止不同迴圈存有依賴關係,如此有助於編寫出可向量化的程式碼。
GPU架構
當需處理的資料量不斷增加時,就必須採用能擴展運算規模的平行運算系統。基於成本和通用性的考量,用戶通常會選擇同屬單指令多資料流(SIMD)架構的CPU或GPU。SIMD是使用單一指令對不同的資料,同時執行多個作業。雖然,CPU也支援SIMD,但其效能沒有GPU高。
如圖1,與CPU相比,繪圖處理器(Graphics Processing Unit, GPU)具有較多的處理器、較高的記憶體頻寬,而且擁有更複雜的指令處理過程和更快的時脈速度。CPU則比GPU擁有更多的高速緩存記憶體(Cache Memory)。然而,CPU的算術邏輯單元(ALU)和浮點運算單元(FPU)的數量比GPU少。因此,通常會使用CPU來執行流程很複雜的工作,而使用GPU來處理大量且密集的運算任務。
圖1 CPU和GPU硬體架構之比較
GPU旨在利用其大規模的平行架構,產生高運算速率。通常,GPU的運算力是用每秒數十億(Giga)次浮點運算(GFLOPS)、或每秒數兆(Tera)次浮點運算(TFLOPS)來計量。不過,目前最新的計量單位已達每秒一千兆(Pera)次浮點運算(PFLOPS)。GPU硬體有標準顯示卡、高階加速卡等不同規格。GPU的圖形管道(Graphics Pipeline)之所以能具有平行化和高運算速率,是因為下列的兩個關鍵特性:
- 物件(Object)的獨立性:一個典型的圖形場景是由許多獨立的物件組成的,且每個物件都可以單獨處理,不依賴其他物件。
- 統一的處理步驟:所有物件的處理步驟的順序都是相同的。
GPU的串流多處理器(Streaming Multiprocessor, SM)類似CPU中的核心。而GPU的核心類似於CPU中的向量通道(Vector Lane)。SM是內含核心的硬體單元。當一個函式(Function)或核心(Kernel)程式在GPU上運行時,它通常會被分解為數個執行緒區塊(Thread Block)。這些執行緒區塊包含許多個執行緒,如圖2。每一個SM可以管理其核心或串流處理器(Stream Processor, SP)上的執行緒,如圖3。如果執行緒區塊的數量多於SM,則可以將多個執行緒區塊指派給單一SM。
圖2 GPU中的執行緒和執行緒區塊的軟體、硬體對應關係
此外,多個執行緒也可以在單一核心上運行。每一個SM又將執行緒區塊分成數個稱為經線(Warp)的群組,每條經線包含32個執行緒。這些執行緒遵循SIMD架構,對不同資料元素執行相同的指令流。在NVIDIA的CUDA架構中,經線的大小是設為32,這是因為CUDA核心被分成32個。這使得經線中的所有執行緒能夠由32個CUDA核心平行處理,實現高效率運算和使資源利用最佳化。
在SIMD中,單一指令統一作用於所有資料元素,每個資料元素以完全相同的方式處理。但在GPU中,常使用單指令多執行緒(Single Instruction/Multiple Thread, SIMT)放寬了這個限制。如圖3,在SIMT中,可以啟動或停用執行緒,指令和資料只能在啟動的執行緒中處理,而停用的執行緒中的資料則保持不變。
圖3 GPU的單指令多執行緒(SIMT)架構
異質運算
雖然,GPU可加快密集型運算的速度,但也需要負責串列或序列處理的CPU來管理輸出入裝置、事先準備好適用於GPU的資料,以及處理複雜的工作流程或邏輯。因為是跨不同硬體架構,所以稱之為異質運算。另一方面,應用程式通常是屬於C或C++等高階語言,但因為機器無法直接處理高階語言的程式指令,因此必須由編譯器將應用程式碼轉換為低階的二進制碼。
雖然GPU和CPU都可以執行相同的核心程式,但當橫跨不同的硬體平台時,為了確保相容性,需要使用指令或參數在指定的虛擬架構內,先執行特定的程式碼,藉此編譯並產生該架構(GPU或CPU)的指令集和二進制碼,這類似跨平台編譯(Cross-Compiling)的功能。目前常用的能夠橫跨GPU和CPU平台的編碼框架有:SYCL、CUDA和Kokkos,利用它們可直接為不同硬體架構編寫核心程式或函式。下面將使用Kokkos的範例來說明。
Kokkos是一種開源的C++程式設計模型,用於編寫可移植的核心程式。如圖4,也可將Kokkos視為C++的模板程式庫(Template Library)。將它安裝在CUDA、OpenMP和其它平行後端(Parallel Backend)軟體上面,旨在描述或定義工作內容。Kokkos的核心為平行演算法提供了一種程式設計模型,此模型使用多核心晶片,並在這些核心晶片之間共享記憶體。此平行運算的核心程式包含下列三個組件:
- 模式(Pattern):定義整個編碼框架或平行運算的結構。常用的模式有:for迴圈、掃描(Scan)、歸約(Reduction)、任務圖(Task-Graph)。掃描模式是將一個陣列中的第i元素之前的所有元素相加後的總和,它有兩種:一種是將第i元素也相加的掃描、另一種是不將第i元素相加的掃描。例如:A=[1, 2, 3, 4, 5],若是將A的第i元素也相加的掃描模式,結果A是等於[1, 3, 6, 10, 15]。
圖4(a) Kokkos和上下層軟硬體的協同關係:(a)連接序列後端(Serial Backend)
圖4(b) Kokkos和上下層軟硬體的協同關係:(b)連接平行後端(Parallel Backend)
歸約模式是使用一個關聯運算子(Associative Operator)將一個陣列中的元素組合起來的過程,例如:求出所有元素相加之和,或尋找最大值。任務圖模式是使用節點來表示任務,並使用邊(Edge)來表示不同任務之間的依賴關係。圖5是一個影像處理管道的任務圖,按照執行的先後順序,包含四個任務:載入圖片(任務A)、應用過濾器(任務B)、偵測邊緣(任務C)、儲存處理後的圖像(任務D)。而且,後一任務取決於或依賴於前一任務。
- 執行策略(Execution Policy):決定如何管理和調度平行運算。它通常包含:靜態調度(Static Scheduling)、動態調度(Dynamic Scheduling)、執行緒群組(Thread Team)。靜態調度是在運算開始時,工作負載能在執行緒或處理單元之間平均分擔。動態調度是當有執行緒或處理單元可供利用時,能動態地分配工作負載。執行緒群組是由數個執行緒構成一個群組,而且數個執行緒群組能平行化地執行任務。
- 計算體(Computational Body):實際執行每一個工作單元的程式碼。例如:循環體(Loop Body)。這是每一個循環迴圈或工作單元都有被實際執行的程式碼。它定義了要對資料執行的作業項目,例如:對陣列中的元素執行算術運算的循環體。
圖5 一個影像處理管道的任務圖
模式和執行策略驅動計算體。例如:for迴圈是模式;控制模式的條件是執行策略,譬如:(element=0; element
Kokkos編碼框架
Kokkos編碼框架是根據下列三個關鍵因素定義參數和方法(Method):
- 執行空間:決定程式碼將在哪裡運行。
- 記憶體空間:決定將使用哪些記憶體資源。
- 資料結構和資料管理:決定如何建置和管理資料。
Kokkos有提供記憶體空間的選項,讓用戶能夠在不同的運算平台上,管理記憶體和存放資料(Data Placement)。如圖6,常用的記憶體空間有:
- HostSpace:此記憶體空間代表CPU的主記憶體。
- CudaSpace:CudaSpace使用於具有CUDA的NVIDIA GPU。它負責GPU的記憶體分配和管理,實現高效率的資料傳輸和運算。
- CudaUVMSpace:CudaUVMSpace是CUDA中利用統一虛擬記憶體(Unified Virtual Memory, UVM)的特定記憶體空間。當在此記憶體空間中分配記憶體時,CPU和GPU都可以存取它,且兩者能正確地共享資料。此外,當系統效能受到影響時,CUDA能及時介入,自動移動資料。
圖6(a) Kokkos常用的記憶體空間:(a)HostSpace
圖說
圖6(c) Kokkos常用的記憶體空間:(c)CudaUVMSpace
Kokkos還支援記憶體布局(Memory Layout),這是指在記憶體中的資料配置與排列。Kokkos能針對不同的計算,採取最佳的資料儲存方案。常用的記憶體布局有:行優先(Row-Major Order)和列優先(Column-Major Order),如圖7。在C和C++程式語言中,多維陣列預設的記憶體布局是行優先,最右邊的元素在記憶體中的變化最快。但在列優先中,最左邊的元素在記憶體中的變化最快。
圖7 在記憶體中儲存多維陣列的方法:行優先和列優先
Kokkos的記憶體空間若沒有特別設定,HostSpace預設的記憶體布局是行優先。CudaSpace預設的記憶體布局則是列優先。在Kokkos程式中,可以使用不同的巨集(Macro)來分別表示CPU和GPU的記憶體空間和記憶體布局,並以編譯旗標來區分它們。
此外,Kokkos還提供所謂視圖(View)的基本資料結構,以及鏡射視圖(Mirror View)功能,後者對於先經過CPU讀取資料,然後在GPU上處理此資料之類的工作很有幫助。鏡射視圖可視為主資料的鏡射副本。如圖8,可以使用Kokkos的create_mirror_view()函式,在指定的執行空間,譬如在GPU中,產生與主視圖相同資料類型和相同維度的鏡射視圖。
圖8 Kokkos的鏡射視圖功能