天天看點

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

本節書摘來自華章社群《cuda c程式設計權威指南》一書中的第2章,第2.1節cuda程式設計模型概述,作者[美] 馬克斯·格羅斯曼(max grossman),更多章節内容可以通路雲栖社群“華章社群”公衆号檢視

2.1 cuda程式設計模型概述

cuda程式設計模型提供了一個計算機架構抽象作為應用程式和其可用硬體之間的橋梁。圖2-1說明了程式和程式設計模型實作之間的抽象結構的重要。通信抽象是程式與程式設計模型實作之間的分界線,它通過專業的硬體原語和作業系統的編譯器或庫來實作。利用程式設計模型所編寫的程式指定了程式的各組成部分是如何共享資訊及互相協作的。程式設計模型從邏輯上提供了一個特定的計算機架構,通常它展現在程式設計語言或程式設計環境中。

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

除了與其他并行程式設計模型共有的抽象外,cuda程式設計模型還利用gpu架構的計算能力提供了以下幾個特有功能。

一種通過層次結構在gpu中組織線程的方法

一種通過層次結構在gpu中通路記憶體的方法

在本章和下一章你将重點學習第一個主題,而在第4章和第5章将學習第二個主題。

以程式員的角度可以從以下幾個不同的層面來看待并行計算。

領域層

邏輯層

硬體層

在程式設計與算法設計的過程中,你最關心的應是在領域層如何解析資料和函數,以便在并行運作環境中能正确、高效地解決問題。當進入程式設計階段,你的關注點應轉向如何組織并發線程。在這個階段,你需要從邏輯層面來思考,以確定你的線程和計算能正确地解決問題。在c語言并行程式設計中,需要使用pthreads或openmp技術來顯式地管理線程。cuda提出了一個線程層次結構抽象的概念,以允許控制線程行為。在閱讀本書中的示例時,你會發現這個抽象為并行程式設計提供了良好的可擴充性。在硬體層,通過了解線程是如何映射到核心可以幫助提高其性能。cuda線程模型在不強調較低級别細節的情況下提供了充足的資訊,具體内容詳見第3章。

2.1.1 cuda程式設計結構

cuda程式設計模型使用由c語言擴充生成的注釋代碼在異構計算系統中執行應用程式。在一個異構環境中包含多個cpu和gpu,每個gpu和cpu的記憶體都由一條pci-express總線分隔開。是以,需要注意區分以下内容。

主機:cpu及其記憶體(主機記憶體)

裝置:gpu及其記憶體(裝置記憶體)

為了清楚地指明不同的記憶體空間,在本書的示例代碼中,主機記憶體中的變量名以h_為字首,裝置記憶體中的變量名以d_為字首。

從cuda 6.0開始,nvidia提出了名為“統一尋址”(unified memory)的程式設計模型的改進,它連接配接了主機記憶體和裝置記憶體空間,可使用單個指針通路cpu和gpu記憶體,無須彼此之間手動拷貝資料。更多細節詳見第4章。現在,重要的是應學會如何為主機和裝置配置設定記憶體空間以及如何在cpu和gpu之間拷貝共享資料。這種程式員管理模式控制下的記憶體和資料可以優化應用程式并實作硬體系統使用率的最大化。

核心(kernel)是cuda程式設計模型的一個重要組成部分,其代碼在gpu上運作。作為一個開發人員,你可以串行執行核函數。在此背景下,cuda的排程管理程式員在gpu線程上編寫核函數。在主機上,基于應用程式資料以及gpu的性能定義如何讓裝置實作算法功能。這樣做的目的是使你專注于算法的邏輯(通過編寫串行代碼),且在建立和管理大量的gpu線程時不必拘泥于細節。

多數情況下,主機可以獨立地對裝置進行操作。核心一旦被啟動,管理權立刻傳回給主機,釋放cpu來執行由裝置上運作的并行代碼實作的額外的任務。cuda程式設計模型主要是異步的,是以在gpu上進行的運算可以與主機-裝置通信重疊。一個典型的cuda程式包括由并行代碼互補的串行代碼。如圖2-2所示,串行代碼(及任務并行代碼)在主機cpu上執行,而并行代碼在gpu上執行。主機代碼按照ansi c标準進行編寫,而裝置代碼使用cuda c進行編寫。你可以将所有的代碼統一放在一個源檔案中,也可以使用多個源檔案來建構應用程式和庫。nvidia 的c編譯器(nvcc)為主機和裝置生成可執行代碼。

一個典型的cuda程式實作流程遵循以下模式。

1.把資料從cpu記憶體拷貝到gpu記憶體。

調用核函數對存儲在gpu記憶體中的資料進行操作。

将資料從gpu記憶體傳送回到cpu記憶體。

首先,你要學習的是記憶體管理及主機和裝置之間的資料傳輸。在本章後面你将學到更多gpu核函數執行的細節内容。

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

2.1.2 記憶體管理

cuda程式設計模型假設系統是由一個主機和一個裝置組成的,而且各自擁有獨立的記憶體。核函數是在裝置上運作的。為使你擁有充分的控制權并使系統達到最佳性能,cuda運作時負責配置設定與釋放裝置記憶體,并且在主機記憶體和裝置記憶體之間傳輸資料。表2-1列出了标準的c函數以及相應地針對記憶體操作的cuda c函數。

用于執行gpu記憶體配置設定的是cudamalloc函數,其函數原型為:

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

https://yqfile.alicdn.com/26818766f14b3d2f15d0787586ca8b19619515e8.png" >

cudageterrorstring函數和c語言中的strerror函數類似。

cuda程式設計模型從gpu架構中抽象出一個記憶體層次結構。圖2-3所示的是一個簡化的gpu記憶體結構,它主要包含兩部分:全局記憶體和共享記憶體。第4章和第5章詳細介紹了gpu記憶體層次結構的内容。

記憶體層次結構

cuda程式設計模型最顯著的一個特點就是揭示了記憶體層次結構。每一個gpu裝置都有用于不同用途的存儲類型。在第4章和第5章将會詳細介紹。

在gpu記憶體層次結構中,最主要的兩種記憶體是全局記憶體和共享記憶體。全局類似于cpu的系統記憶體,而共享記憶體類似于cpu的緩存。然而gpu的共享記憶體可以由cuda c的核心直接控制。

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述
《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

而不是用:

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

應用程式在運作時将會崩潰。

為了避免這類錯誤,cuda 6.0提出了統一尋址,使用一個指針來通路cpu和gpu的記憶體。有關統一尋址的内容詳見第4章。

2.1.3 線程管理

當核函數在主機端啟動時,它的執行會移動到裝置上,此時裝置中會産生大量的線程并且每個線程都執行由核函數指定的語句。了解如何組織線程是cuda程式設計的一個關鍵部分。cuda明确了線程層次抽象的概念以便于你組織線程。這是一個兩層的線程層次結構,由線程塊和線程塊網格構成,如圖2-5所示。

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

由一個核心啟動所産生的所有線程統稱為一個網格。同一網格中的所有線程共享相同的全局記憶體空間。一個網格由多個線程塊構成,一個線程塊包含一組線程,同一線程塊内的線程協作可以通過以下方式來實作。

同步

共享記憶體

不同塊内的線程不能協作。

線程依靠以下兩個坐标變量來區分彼此。

blockidx(線程塊線上程格内的索引)

threadidx(塊内的線程索引)

這些變量是核函數中需要預初始化的内置變量。當執行一個核函數時,cuda運作時為每個線程配置設定坐标變量blockidx和threadidx。基于這些坐标,你可以将部分資料配置設定給不同的線程。

該坐标變量是基于uint3定義的cuda内置的向量類型,是一個包含3個無符号整數的結構,可以通過x、y、z三個字段來指定。

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

網格和線程塊的次元

通常,一個線程格會被組織成線程塊的二維數組形式,一個線程塊會被組織成線程的三維數組形式。

線程格和線程塊均使用3個dim3類型的無符号整型字段,而未使用的字段将被初始化為1且忽略不計。

在cuda程式中有兩組不同的網格和塊變量:手動定義的dim3資料類型和預定義的uint3資料類型。在主機端,作為核心調用的一部分,你可以使用dim3資料類型定義一個網格和塊的次元。當執行核函數時,cuda運作時會生成相應的内置預初始化的網格、塊和線程變量,它們在核函數内均可被通路到且為unit3類型。手動定義的dim3類型的網格和塊變量僅在主機端可見,而unit3類型的内置預初始化的網格和塊變量僅在裝置端可見。

你可以通過代碼清單2-2來驗證這些變量如何使用。首先,定義程式所用的資料大小,為了對此進行說明,我們定義一個較小的資料。

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

把代碼合并儲存成名為checkdimension.cu的檔案,如代碼清單2-2所示。

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述
《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

對于一個給定的資料大小,确定網格和塊尺寸的一般步驟為:

确定塊的大小

在已知資料大小和塊大小的基礎上計算網格次元

要确定塊尺寸,通常需要考慮:

核心的性能特性

gpu資源的限制

本書的後續章節會對以上幾點因素進行詳細介紹。代碼清單2-3使用了一個一維網格和一個一維塊來說明當塊的大小改變時,網格的尺寸也會随之改變。

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

網格和塊的次元存在幾個限制因素,對于塊大小的一個主要限制因素就是可利用的計算資源,如寄存器,共享記憶體等。某些限制可以通過查詢gpu裝置撤回。

網格和塊從邏輯上代表了一個核函數的線程層次結構。在第3章中,你會發現這種線程組織方式能使你在不同的裝置上有效地執行相同的程式代碼,而且每一個線程組織具有不同數量的計算和記憶體資源。

2.1.4 啟動一個cuda核函數

你應該對下列c語言函數調用語句很熟悉:

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

由于資料在全局記憶體中是線性存儲的,是以可以用變量blockidx.x和threadid.x來進行以下操作。

在網格中辨別一個唯一的線程

建立線程和資料元素之間的映射關系

如果把所有32個元素放到一個塊裡,那麼隻會得到一個塊:

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

之前所有的核函數調用完成後開始拷貝資料。當拷貝完成後,控制權立刻傳回給主機端。

異步行為

不同于c語言的函數調用,所有的cuda核函數的啟動都是異步的。cuda核心調用完成後,控制權立刻傳回給cpu。

2.1.5 編寫核函數

核函數是在裝置端執行的代碼。在核函數中,需要為一個線程規定要進行的計算以及要進行的資料通路。當核函數被調用時,許多不同的cuda線程并行執行同一個計算任務。以下是用__global__聲明定義核函數:

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

考慮一個簡單的例子:将兩個大小為n的向量a和b相加,主機端的向量加法的c代碼如下:

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

2.1.6 驗證核函數

既然你已經編寫了核函數,你如何能知道它是否正确運作?你需要一個主機函數來驗證核函數的結果。

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

驗證核函數代碼

除了許多可用的調試工具外,還有兩個非常簡單實用的方法可以驗證核函數。

首先,你可以在fermi及更高版本的裝置端的核函數中使用printf函數。

其次,可以将執行參數設定為<<<1,1>>>,是以強制用一個塊和一個線程執行核函數,這模拟了串行執行程式。這對于調試和驗證結果是否正确是非常有用的,而且,如果你遇到了運算次序的問題,這有助于你對比驗證數值結果是否是按位精确的。

2.1.7 處理錯誤

由于許多cuda調用是異步的,是以有時可能很難确定某個錯誤是由哪一步程式引起的。定義一個錯誤處理宏封裝所有的cuda api調用,這簡化了錯誤檢查過程:

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

check(cudadevicesynchronize())會阻塞主機端線程的運作直到裝置端所有的請求任務都結束,并確定最後的核函數啟動部分不會出錯。以上僅是以調試為目的的,因為在核函數啟動後添加這個檢查點會阻塞主機端線程,使該檢查點成為全局屏障。

2.1.8 編譯和執行

現在把所有的代碼放在一個檔案名為sumarraysongpu-small-case.cu的檔案中,如代碼清單2-4所示。

代碼清單2-4 基于gpu的向量加法(sumarraysongpu-small-case.cu)

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述
《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述
《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

在這段代碼中,向量大小被設定為32,如下所示:

《CUDA C程式設計權威指南》——2.1節CUDA程式設計模型概述

你需要確定一般情況下進行更改所産生結果的正确性。

繼續閱讀