【CUDA】grid、block、thread的關係及thread索引的計算
由於專案需要用到GPU,所以最近開始學習CUDA程式設計模型,剛開始接觸,先搞清楚執行緒關係和記憶體模型是非常重要的,但是發現書上和許多部落格關於執行緒這些關係沒講明白,所以就著自己的理解,做點筆記,歡迎討論。
這篇文章針對於已經瞭解過了CUDA執行緒的相關知識,最好已經動手寫過CUDA C的程式碼,而對並行執行緒感到迷惑,不知道怎麼計算執行緒索引的讀者,如果沒接觸過,那麼先看看書,敲兩段程式碼跑跑,如果你理解了那麼恭喜你,如果還有疑惑,那麼再來看看這篇文章,或許有幫助。
首先,認識一下執行緒。(⊙o⊙)…雖然畫成這樣總是感覺有點怪怪的,但是你應該已經見怪不怪了吧~
下面我們來看一段程式碼,其功能是對兩個陣列求和,並儲存到另一個數組,很簡單吧~
#include <cuda_runtime.h> #include <device_launch_parameters.h> #include <iostream> using namespace std; // 二:執行緒執行程式碼 __global__ void vector_add(float* vec1, float* vec2, float* vecres, int length) { int tid = threadIdx.x; if (tid < length) { vecres[tid] = vec1[tid] + vec2[tid]; } } int main() { const int length = 16; // 陣列長度為16 float a[length], b[length], c[length]; // host中的陣列 for (int i = 0; i < length; i++) { // 初始賦值 a[i] = b[i] = i; } float* a_device, *b_device, *c_device; // device中的陣列 cudaMalloc((void**)&a_device, length * sizeof(float)); // 分配記憶體 cudaMalloc((void**)&b_device, length * sizeof(float)); cudaMalloc((void**)&c_device, length * sizeof(float)); cudaMemcpy(a_device, a, length * sizeof(float), cudaMemcpyHostToDevice); // 將host陣列的值拷貝給device陣列 cudaMemcpy(b_device, b, length * sizeof(float), cudaMemcpyHostToDevice); // 一:引數配置 dim3 grid(1, 1, 1), block(length, 1, 1); // 設定引數 vector_add<<<grid,block>>>(a_device, b_device, c_device, length); // 啟動kernel cudaMemcpy(c, c_device, length * sizeof(float), cudaMemcpyDeviceToHost); // 將結果拷貝到host for (int i = 0; i < length; i++) { // 打印出來方便觀察 cout << c[i] << " "; } cout << endl; system("pause"); return 0; }
執行結果:
結果是對的,也是我們所能預料到的。那麼現在我們來分析程式碼中註釋的一、二處究竟該怎麼來寫。
首先,我們要明白,上面的程式碼計算的是兩個一維向量的和。由於陣列大小是16,所以我們使用了16個執行緒來計算。
dim3 grid(1, 1, 1), block(length, 1, 1); // 設定引數
先說grid,在這段程式碼中,我們設定引數為執行緒格(grid)中只有一個一維的block,該block的x維度上有16個,這個應該一下就看出來啦。因為grid(x,y,z)中的x=1,y=1,z=1,即各個維度均為1,所以是一維的,數量為x*y*z=1*1*1=1。如果沒明白,再看兩個例子:
dim3 grid1(2, 1, 1); // x=2, y=1, z=1
dim3 grid2(4, 2, 1); // x=4, y=2, z=1
dim3 grid3(2, 3, 4); // x=2, y=3, z=4
可以知道,grid1是一維的(因為y,z維度是1),grid2是二維的(因為z維度是1),grid3是三維的,且grid1,grid2,grid3中分別有2、8、24個block。
同理,對於執行緒塊(block),我們知道之前的程式碼中,block中存在16個執行緒,且該執行緒塊維度是一維的,因為block(x,y,z)中x=length=16,y=1,z=1。
我畫個圖來幫助理解,大概就是這樣子的:
dim3 grid(1, 1, 1), block(length, 1, 1); // 設定引數
OK,我想這下應該就清楚了,就是一個一維的block(此處只有x維度上存在16個執行緒)。所以,內建變數只有一個在起作用,就是threadIdx.x,它的範圍是[0,15]。因此,我們在計算執行緒索引是,只用這個內建變數就行了(其他的為0,寫了也不起作用):
// 二:執行緒執行程式碼
__global__ void vector_add(float* vec1, float* vec2, float* vecres, int length) {
int tid = threadIdx.x; // 只使用了threadIdx.x
if (tid < length) {
vecres[tid] = vec1[tid] + vec2[tid];
}
}
OK,看到這裡,你可能還是不大明白什麼一維二維的,我們再來看一個:
dim3 grid(1, 1, 1), block(8, 2, 1); // 設定引數
根據上面的介紹,我們知道這個執行緒格只有一個一維的執行緒塊,該執行緒塊內的執行緒是二維的,x的維度為8,y的維度為2,共有8*2=16個執行緒,如果要用這16個執行緒來計算陣列的累加,當然是可以的,但是我們這裡需要改動一下執行緒執行程式碼中的索引計算方式了。
// 二:執行緒執行程式碼
__global__ void vector_add(float* vec1, float* vec2, float* vecres, int length) {
int tid = threadIdx.y * blockDim.x + threadIdx.x; // 使用了threadIdx.x, threadIdx.x, blockDim.x
if (tid < length) {
vecres[tid] = vec1[tid] + vec2[tid];
}
}
我們一定要有並行思想,這裡有16個執行緒,kernel啟動後,每個執行緒都有自己的索引號,比如某個執行緒位於grid中哪個維度的block(即blockIdx.x,block.y,block.z),又位於該block的哪個維度的執行緒(即threadIdx.x,threadIdx.y,threadIdx.z),利用這些執行緒索引號對映到對應的陣列下標,我們要做的工作就是將保證這些下標不重複(如果重複的話,那就慘了),最初那種一維的計算方式就不行了。因此,通過使用threadIdx,blockDim來進行對映(偏移)。blockDim.x=8,blockDim.y=2,如上程式碼。
其實,我感覺有些我不能用文字準確、清晰的描述出來,所以咯,我們再來一個例子吧,我相信,多看一看,多想一想就明白了。
dim3 grid(1, 1, 1), block(4, 4, 1); // 設定引數
我們將block改成上面的這樣,其執行緒模型為下圖:
當然,kernel函式的程式碼依然可以不用變動,這個應該想得清楚,還是再寫一下吧。
// 二:執行緒執行程式碼
__global__ void vector_add(float* vec1, float* vec2, float* vecres, int length) {
int tid = threadIdx.y * blockDim.x + threadIdx.x; // 使用了threadIdx.x, threadIdx.x, blockDim.x
if (tid < length) {
vecres[tid] = vec1[tid] + vec2[tid];
}
}
—————————————————————————————————————————————————————————————————————————————
以上內容我們分別介紹了用一維和二維執行緒來計算一維陣列的求和,實際上陣列的維度與執行緒格、執行緒塊和執行緒的維度並不是那麼密不可分的,都可以組合實現,只不過在實現時,良好的引數配置對索引的計算很方便,而且由於grid、block、thread維度的限制,還有warpSize的限制,所以對於較大的資料量來說,我們應該做到心中有數,進行有效的塊分解。
現在來看看二維的block,在整個文章中,我只講解一維、二維的,因為三維的我不知道怎麼畫圖啦,而且不好描述,免得誤導大家。
還是上面的一維陣列,長度為16。
dim3 grid(16, 1, 1), block(1, 1, 1); // 設定引數
先來個執行緒模型圖,我想大家並不會感到驚訝,綠色的區域表示grid,藍色的區域表示block,圖中有一個grid和16個block,每個block都是一維,而且x維度上只有一個執行緒的:
顯然,我們的執行緒索引程式碼應該為如下:
// 二:執行緒執行程式碼
__global__ void vector_add(float* vec1, float* vec2, float* vecres, int length) {
int tid = blockIdx.x;
if (tid < length) {
vecres[tid] = vec1[tid] + vec2[tid];
}
}
或許你會有疑惑,那麼我們再來看一個:
dim3 grid(4, 1, 1), block(4, 1, 1);
執行緒索引程式碼應該為如下:
// 二:執行緒執行程式碼
__global__ void vector_add(float* vec1, float* vec2, float* vecres, int length) {
int tid = blockIdx.x * blockDim.x + threadIdx.x;
if (tid < length) {
vecres[tid] = vec1[tid] + vec2[tid];
}
}
到現在為止,我覺得你應該有所領悟。如果還是不曉得的話,我想你應該認認真真的看圖並動手分析了,圖中的每一個塊,每一個字都是有它的作用的,你不應該就此放棄。
我依然相信,能用圖解決,就不嗶嗶。就好像你給一個人描述一座宮殿是多麼多麼的巨集偉,富麗堂皇,他並不不會感冒。你就說,嘿大傻,給你瞧瞧我去歐洲玩的教堂,這是照片,不用多說,大傻自己就知道了。
比如,我描述說:
dim3 grid(2, 2, 1), block(2, 2, 1);
這樣肯定不直觀,那我再給你一幅示意圖:
那麼執行程式碼及索引計算如下:
// 二:執行緒執行程式碼
__global__ void vector_add(float* vec1, float* vec2, float* vecres, int length) {
// 在第幾個塊中 * 塊的大小 + 塊中的x, y維度(幾行幾列)
int tid = (blockIdx.y * gridDim.x + blockIdx.x) * (blockDim.x * blockDim.y) + threadIdx.y * blockDim.x + threadIdx.x;
if (tid < length) {
vecres[tid] = vec1[tid] + vec2[tid];
}
}
上面的程式碼可能要複雜一點,但是你慢慢的會發現這很有趣。
到此,我想講的就完了。當然對於二維的陣列或是三維的陣列,我想多看幾個例子也就會有體會了。
這裡還是忍不住要吐槽一下內建變數threadIdx和blockIdx的命名了,每次看到這些內建變數其最後一個字母是x,就會給我一種誤會是x維度上的,我覺得使用threadId和blockId是多麼的良好。當然,勝利的總是API一方,我也只能吐吐槽。
—————————————————————————————————————————————————————————————————————————————
最後再來一發,我給個圖,我們來倒推其引數及相關執行程式碼,如下:
由於上傳圖片大小限制,由BMP轉成JPG格式的了,有點不清晰,但足夠看了。
顯然引數為:
dim3 grid(8, 4, 1), block(8, 2, 1);
共有8*4*8*2=512個執行緒,當然在CUDA程式設計中,這算很少的了。如果是一幅512x512大小的影象做加或點乘之類的運算,隨隨便便就是幾十萬的執行緒數了。
萬變不離其宗,其一維的計算方式如下:
__global__ void vector_add(float* vec1, float* vec2, float* vecres, int length) {
// 在第幾個塊中 * 塊的大小 + 塊中的x, y維度(幾行幾列)
int tid = (blockIdx.y * gridDim.x + blockIdx.x) * (blockDim.x * blockDim.y) + threadIdx.y * blockDim.x + threadIdx.x;
if (tid < length) {
vecres[tid] = vec1[tid] + vec2[tid];
}
}
再給出二維的:
__global__ void vector_add(float** mat1, float** mat2, float** matres, int width) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x < width && y < width) {
matres[x][y] = mat1[x][y] + mat2[x][y];
}
}
---------------------
作者:hujingshuang
來源:CSDN
原文:https://blog.csdn.net/hujingshuang/article/details/53097222
版權宣告:本文為博主原創文章,轉載請附上博文連結!
相關推薦
【CUDA】grid、block、thread的關係及thread索引的計算
由於專案需要用到GPU,所以最近開始學習CUDA程式設計模型,剛開始接觸,先搞清楚執行緒關係和記憶體模型是非常重要的,但是發現書上和許多部落格關於執行緒這些關係沒講明白,所以就著自己的理解,做點筆記,歡迎討論。 這篇文章針對於已經瞭解過了CUDA執行緒的
CUDA中grid、block、thread、warp與SM、SP的關係
首先概括一下這幾個概念。其中SM(Streaming Multiprocessor)和SP(streaming Processor)是硬體層次的,其中一個SM可以包含多個SP。thread是一個執行緒,多個thread組成一個執行緒塊block,多個block又組成一個執行緒網格grid。 現在
【精】iOS知識樹,知識點(包括物件、Block、訊息轉發、GCD、執行時、runloop、動畫、Push、KVO、tableview,UIViewController、提交AppStore)
本文旨在總結iOS知識網路,知識點,該知識網路羅列出常見UIKit、Foundation的物件特點和一些使用經驗,可以看成是一本書;文字編輯採用樹的形式,對知識點進行羅列,並標註一些使用經驗(★)希望對初學者有用或給一些解決疑難雜症者提供思路;某些知識點會深入
【轉】shell expect spawn、linux expect 用法小記 看著舒服點
ssh username 英文 認識 exe 無法找到 usr 過去 操作 使用expect實現自動登錄的腳本,網上有很多,可是都沒有一個明白的說明,初學者一般都是照抄、收藏。可是為什麽要這麽寫卻不知其然。本文用一個最短的例子說明腳本的原理。 腳本代碼如下: #####
面向對象【day08】:靜態方法、類方法、屬性方法
name 每次 對象 sha 飛走了 tee func ssm [0 本節內容 概述 靜態方法 類方法 屬性方法 總結 一、概述 前面我們已經講解了關於類的很多東西,今天講講類的另外的特性:靜態方法(staticmethod)、類方法(classmethod)、屬性
【Quartz】Quartz的搭建、應用(單獨使用Quartz)
文章 sgd aca guide mfc uci strong div guid 原文:http://www.cnblogs.com/nick-huang/p/4848843.html 目錄 1. > 參考的優秀資料 2. > 版本說明 3. > 簡單的
【十】叠代器、生成器、裝飾器和標準庫
ber 遍歷 mmu 取出 out most 例如 list char 一:叠代器 在python中,很多對象可以直接通過for語句來直接遍歷,例如:list、string、dict等等,這些被稱為可叠代對象 叠代器是一個可以I記住遍歷的位置的對象。 在python中,支持
【python】字符串、16進制等數據處理
python binascii 轉碼最近做一個socket server,需要接收組播報文,並進行分析處理。其中涉及的一個問題是,待發送的報文是字符串形式,類似“hello world”。從wireshark截取的報文看,都是16進制數據,以為必須轉為該種類型才能發送,需要轉換為16進制字符串,類似“0x\a
010-shiro與spring web項目整合【四】緩存Ehcache、Redis
principal eba view event ica inter element edi value 一、Ehcache shiro每次授權都會通過realm獲取權限信息,為了提高訪問速度需要添加緩存,第一次從realm中讀取權限數據,之後不再讀取,這裏Shiro和E
【java】i++與++i、i--運算
log 原理 ring 自加 clas rgs stat static system 1 package test; 2 3 //i++與--i運算 4 public class test { 5 6 public static void main
【轉】QT中QWidget、QDialog及QMainWindow的區別
屏幕 編輯 派生 標記 裝飾 按鈕 set 沒有 idg QWidget類是所有用戶界面對象的基類。 窗口部件是用戶界面的一個基本單元:它從窗口系統接收鼠標、鍵盤和其它事件,並且在屏幕上繪制自己。每一個窗口部件都是矩形的,並且它們按Z軸順序排列。一個窗口部件可以被它的父窗口
【轉】開源許可證GPL、BSD、MIT、Mozilla、Apache和LGPL的區別
2.0 源程序 組織 alt 要求 控制 知識產權 bsp script 首先借用有心人士的一張相當直觀清晰的圖來劃分各種協議:開源許可證GPL、BSD、MIT、Mozilla、Apache和LGPL的區別 以下是上述協議的簡單介紹:BSD開源協議BSD開源協議是一個
【轉載】編譯型語言、解釋型語言、靜態類型語言、動態類型語言概念與區別
自己 運行 是把 修改 lin 鏈接 時代 dll 系統 編譯型語言和解釋型語言 1、編譯型語言 需通過編譯器(compiler)將源代碼編譯成機器碼,之後才能執行的語言。一般需經過編譯(compile)、鏈接(linker)這兩個步驟。編譯是把源代碼編譯成機器碼,鏈接是把
【轉】淺談JavaScript、ES5、ES6
javascrip clas create 支持 ssi 對象 關鍵字 操作符 cnblogs 本文轉自http://www.cnblogs.com/lovesong/p/4908871.html 什麽是JavaScript JavaScript一種動態類型、弱類型、基於原
【轉】C# Linq 交集、並集、差集、去重
log .cn pre tin nio clas int except post 轉自: https://www.cnblogs.com/wdw31210/p/4167306.html using System.Linq; List<string&
【java】java 中 byte[]、File、InputStream 互相轉換
new 文件名 讀寫 文件 tps byte數組 lan http 過程 ========================================================================= 使用過程中,一定要註意close()掉各個讀寫流!!
【SVN】SVN的trunk、branches、tag的使用以及分支的概念
upd 客戶端 code clas 通過 chan verbose 目錄結構 本質 svn的存儲結構一般建議在根目錄下建立trunk、branches、tags這三個文件夾,trunk用於平時的正常工作,branches用於存放各種分支,tags用於存放各種發布版本或某狀態
【Git】(1)---工作區、暫存區、版本庫、遠程倉庫
size 一個 工作區 本地 -s 新建 這樣的 cache 通過 工作區、暫存區、版本庫、遠程倉庫 一、概念 1、四個工作區域 Git本地有四個工作區域:工作目錄(Working Directory)、暫存區(Stage/Index)、資源庫(Repos
adb.【轉】模擬點擊、滑動、輸入、按鍵
按鍵 roi tps mut sta previous col ast ear 1、android adb 模擬點擊、滑動、輸入、按鍵 - 陽光檸檬_的技術筆記 - CSDN博客.html(https://blog.csdn.net/liukang325/article/d
Tomcat學習筆記【1】--- WEB服務器、JavaEE、Tomcat背景
javascrip http .cn 目的 java log 進行 瀏覽器 靜態資源 本文主要講學習Tomcat需要知道的基礎知識。 一 Web服務器 Web服務器可以解析HTTP協議。當Web服務器接收到一個HTTP請求,會返回一個HTTP響應,例如送回一個HTML頁面。